Fixed bug #745636: Added export to openlyrics format with formatting tags.

bzr-revno: 1758
Fixes: https://launchpad.net/bugs/745636
This commit is contained in:
Martin Zibricky 2011-09-23 08:43:06 +02:00 committed by Raoul Snyman
commit b66e07e98b
5 changed files with 401 additions and 113 deletions

View File

@ -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'<span style="-webkit-text-fill-color:red">',
u'end tag': u'{/r}', u'end html': u'</span>', u'protected': True})
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)
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 ``<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):

View File

@ -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', '<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,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'')

View File

@ -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)

View File

@ -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',

View File

@ -61,19 +61,21 @@ The XML of an `OpenLyrics <http://openlyrics.info/>`_ song looks like this::
</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 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):
``<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 @@ class OpenLyrics(object):
``<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 @@ class OpenLyrics(object):
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 @@ 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'<?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 @@ 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 ``<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 @@ 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 <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':
if 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':
if 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, 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 <br/> 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 <line> 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