#
# Gramps - a GTK+/GNOME based genealogy program
#
# Copyright (C) 2009 Benny Malengier
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
"""File and File format management for the different reports
"""
#------------------------------------------------------------------------
#
# Python modules
#
#------------------------------------------------------------------------
from __future__ import print_function
from ...const import GRAMPS_LOCALE as glocale
_ = glocale.translation.gettext
import io
#-------------------------------------------------------------------------
#
# GTK modules
#
#-------------------------------------------------------------------------
#------------------------------------------------------------------------
#
# Gramps modules
#
#------------------------------------------------------------------------
from ...constfunc import cuni
#------------------------------------------------------------------------
#
# Set up logging
#
#------------------------------------------------------------------------
import logging
LOG = logging.getLogger(".docbackend.py")
#------------------------------------------------------------------------
#
# Functions
#
#------------------------------------------------------------------------
[docs]def noescape(text):
"""
Function that does not escape the text passed. Default for backends
"""
return text
#-------------------------------------------------------------------------
#
# DocBackend exception
#
#-------------------------------------------------------------------------
[docs]class DocBackendError(Exception):
"""Error used to report docbackend errors."""
def __init__(self, value=""):
Exception.__init__(self)
self.value = value
def __str__(self):
return self.value
#------------------------------------------------------------------------
#
# Document Backend class
#
#------------------------------------------------------------------------
[docs]class DocBackend(object):
"""
Base class for text document backends.
The DocBackend manages a file to which it writes. It further knowns
enough of the file format to be able to translate between the BaseDoc API
and the file format.
Specifically for text reports a translation of styled notes to the file
format usage is done.
"""
BOLD = 0
ITALIC = 1
UNDERLINE = 2
FONTFACE = 3
FONTSIZE = 4
FONTCOLOR = 5
HIGHLIGHT = 6
SUPERSCRIPT = 7
LINK = 8
SUPPORTED_MARKUP = []
ESCAPE_FUNC = lambda: noescape
#Map between styletypes and internally used values. This map is needed
# to make TextDoc officially independant of gen.lib.styledtexttag
STYLETYPE_MAP = {
}
CLASSMAP = None
#STYLETAGTABLE to store markup for write_markup associated with style tags
STYLETAG_MARKUP = {
BOLD : ("", ""),
ITALIC : ("", ""),
UNDERLINE : ("", ""),
SUPERSCRIPT : ("", ""),
LINK : ("", ""),
}
def __init__(self, filename=None):
"""
:param filename: path name of the file the backend works on
"""
self.__file = None
self._filename = filename
[docs] def getf(self):
"""
Obtain the filename on which backend writes
"""
return self._filename
[docs] def setf(self, value):
"""
Set the filename on which the backend writes, changing the value
passed on initialization.
Can only be done if the previous filename is not open.
"""
if self.__file is not None:
raise ValueError(_('Close file first'))
self._filename = value
filename = property(getf, setf, None, "The filename the backend works on")
[docs] def open(self):
"""
Opens the document.
"""
if self.filename is None:
raise DocBackendError(_('No filename given'))
if self.__file is not None :
raise DocBackendError(_('File %s already open, close it first.')
% self.filename)
self._checkfilename()
try:
self.__file = io.open(self.filename, "w", encoding="utf-8")
except IOError as msg:
errmsg = "%s\n%s" % (_("Could not create %s") % self.filename, msg)
raise DocBackendError(errmsg)
except:
raise DocBackendError(_("Could not create %s") % self.filename)
def _checkfilename(self):
"""
Check to make sure filename satisfies the standards for this filetype
"""
pass
[docs] def close(self):
"""
Closes the file that is written on.
"""
if self.__file is None:
raise IOError('No file open')
self.__file.close()
self.__file = None
[docs] def write(self, string):
"""
Write a string to the file. There is no return value.
Due to buffering, the string may not actually show up until the
:meth:`close` method is called.
"""
self.__file.write(string)
[docs] def writelines(self, sequence):
"""
Write a sequence of strings to the file. The sequence can be any
iterable object producing strings, typically a list of strings.
"""
self.__file.writelines(sequence)
[docs] def escape(self, preformatted=False):
"""
The escape func on text for this file format.
:param preformatted: some formats can have different escape function
for normal text and preformatted text
:type preformatted: bool
"""
return self.ESCAPE_FUNC()
[docs] def find_tag_by_stag(self, s_tag):
"""
:param s_tag: object: assumed styledtexttag
:param s_tagvalue: None/int/str: value associated with the tag
A styled tag is type with a value. Every styled tag must be converted
to the tags used in the corresponding markup for the backend,
eg <b>text</b> for bold in html. These markups are stored in
STYLETAG_MARKUP. They are tuples for begin and end tag. If a markup is
not present yet, it is created, using the :meth:`_create_xmltag` method
you can overwrite.
"""
tagtype = s_tag.name
if not self.STYLETYPE_MAP or \
self.CLASSMAP != tagtype.__class__.__name__ :
self.CLASSMAP == tagtype.__class__.__name__
self.STYLETYPE_MAP[tagtype.BOLD] = self.BOLD
self.STYLETYPE_MAP[tagtype.ITALIC] = self.ITALIC
self.STYLETYPE_MAP[tagtype.UNDERLINE] = self.UNDERLINE
self.STYLETYPE_MAP[tagtype.FONTFACE] = self.FONTFACE
self.STYLETYPE_MAP[tagtype.FONTSIZE] = self.FONTSIZE
self.STYLETYPE_MAP[tagtype.FONTCOLOR] = self.FONTCOLOR
self.STYLETYPE_MAP[tagtype.HIGHLIGHT] = self.HIGHLIGHT
self.STYLETYPE_MAP[tagtype.SUPERSCRIPT] = self.SUPERSCRIPT
self.STYLETYPE_MAP[tagtype.LINK] = self.LINK
if s_tag.name == 'Link':
return self.format_link(s_tag.value)
typeval = int(s_tag.name)
s_tagvalue = s_tag.value
tag_name = None
if tagtype.STYLE_TYPE[typeval] == bool:
return self.STYLETAG_MARKUP[self.STYLETYPE_MAP[typeval]]
elif tagtype.STYLE_TYPE[typeval] == str:
tag_name = "%d %s" % (typeval, s_tagvalue)
elif tagtype.STYLE_TYPE[typeval] == int:
tag_name = "%d %d" % (typeval, int(s_tagvalue))
if not tag_name:
return None
tags = self.STYLETAG_MARKUP.get(tag_name)
if tags is not None:
return tags
#no tag known yet, create the markup, add to lookup, and return
tags = self._create_xmltag(self.STYLETYPE_MAP[typeval], s_tagvalue)
self.STYLETAG_MARKUP[tag_name] = tags
return tags
def _create_xmltag(self, tagtype, value):
"""
Create the xmltags for the backend.
Overwrite this method to create functionality with a backend
"""
if tagtype not in self.SUPPORTED_MARKUP:
return None
return ('', '')
[docs] def add_markup_from_styled(self, text, s_tags, split='', escape=True):
"""
Input is plain text, output is text with markup added according to the
s_tags which are assumed to be styledtexttags.
When split is given the text will be split over the value given, and
tags applied in such a way that it the text can be safely splitted in
pieces along split.
:param text: str, a piece of text
:param s_tags: styledtexttags that must be applied to the text
:param split: str, optional. A string along which the output can
be safely split without breaking the styling.
As adding markup means original text must be escaped, ESCAPE_FUNC is
used. This can be used to convert the text of a styledtext to the format
needed for a document backend. Do not call this method in a report,
use the :meth:`write_markup` method.
.. note:: the algorithm is complex as it assumes mixing of tags is not
allowed: eg <b>text<i> here</b> not</i> is assumed invalid
as markup. If the s_tags require such a setup, what is
returned is <b>text</b><i><b> here</b> not</i>
overwrite this method if this complexity is not needed.
"""
if not escape:
escape_func = self.ESCAPE_FUNC
self.ESCAPE_FUNC = lambda: (lambda text: text)
#unicode text must be sliced correctly
text = cuni(text)
FIRST = 0
LAST = 1
tagspos = {}
for s_tag in s_tags:
tag = self.find_tag_by_stag(s_tag)
if tag is not None:
for (start, end) in s_tag.ranges:
if start in tagspos:
tagspos[start] += [(tag, FIRST)]
else:
tagspos[start] = [(tag, FIRST)]
if end in tagspos:
tagspos[end] = [(tag, LAST)] + tagspos[end]
else:
tagspos[end] = [(tag, LAST)]
start = 0
end = len(text)
keylist = list(tagspos.keys())
keylist.sort()
keylist = [x for x in keylist if x <= len(text)]
opentags = []
otext = cuni("") #the output, text with markup
lensplit = len(split)
for pos in keylist:
#write text up to tag
if pos > start:
if split:
#make sure text can split
splitpos = text[start:pos].find(split)
while splitpos != -1:
otext += self.ESCAPE_FUNC()(text[start:start+splitpos])
#close open tags
for opentag in reversed(opentags):
otext += opentag[1]
#add split text
otext += self.ESCAPE_FUNC()(split)
#open the tags again
for opentag in opentags:
otext += opentag[0]
#obtain new values
start = start + splitpos + lensplit
splitpos = text[start:pos].find(split)
otext += self.ESCAPE_FUNC()(text[start:pos])
#write out tags
for tag in tagspos[pos]:
#close open tags starting from last open
for opentag in reversed(opentags):
otext += opentag[1]
#if start, add to opentag in beginning as first to open
if tag[1] == FIRST:
opentags = [tag[0]] + opentags
else:
#end tag, is closed already, remove from opentag
opentags = [x for x in opentags if not x == tag[0] ]
#now all tags are closed, open the ones that should open
for opentag in opentags:
otext += opentag[0]
start = pos
#add remainder of text, no markup present there if all is correct
if opentags:
# a problem, we don't have a closing tag left but there are open
# tags. Just keep them up to end of text
pos = len(text)
print('WARNING: DocBackend : More style tags in text than length '\
'of text allows.\n', opentags)
if pos > start:
if split:
#make sure text can split
splitpos = text[start:pos].find(split)
while splitpos != -1:
otext += self.ESCAPE_FUNC()(text[start:start+splitpos])
#close open tags
for opentag in reversed(opentags):
otext += opentag[1]
#add split text
otext += self.ESCAPE_FUNC()(split)
#open the tags again
for opentag in opentags:
otext += opentag[0]
#obtain new values
start = start + splitpos + lensplit
splitpos = text[start:pos].find(split)
otext += self.ESCAPE_FUNC()(text[start:pos])
for opentag in reversed(opentags):
otext += opentag[1]
else:
otext += self.ESCAPE_FUNC()(text[start:end])
if not escape:
self.ESCAPE_FUNC = escape_func
return otext