"""Module used to treat the footnotes from the hypocratic project.
:Authors: Jonathan Boyle, Nicolas Gruel <nicolas.gruel@manchester.ac.uk>
:Copyright: IT Services, The University of Manchester
"""
# pylint: disable=locally-disabled, invalid-name
import re
from collections import OrderedDict
try:
from .baseclass import Exegis, logger
except ImportError:
from baseclass import Exegis, logger
# Define an Exception
[docs]class Footnote(Exegis):
"""Class Footnote which treat an individual footnote
Attributes
----------
footnote : str
String which contains the footnote to treat.
n_footnote : int
Integer which give the reference number of the footnote treated.
xml : list
list which contains the app XML file.
"""
def __init__(self, footnote=None, n_footnote=None, xml=None):
Exegis.__init__(self)
self.footnote = footnote
self.n_footnote = n_footnote
if xml is None:
xml = []
self.xml = xml
self.wits = []
self._d_footnote = {}
[docs] def check_endnote(self):
"""Method to check if there are a note at the end of a footnote
If the symbol `;` is present in the footnote. Everything after is
considered as a note and will be added as that in the <app>
"""
# Create footnote commentary if present (after `;` symbol)
loc_com = self.footnote.rfind(';')
if loc_com != -1:
self.note_xml(self.footnote[loc_com+1:].strip())
self.footnote = self.footnote[:loc_com]
[docs] def omission(self):
"""Helper function processes a footnote line describing an omission
This helper function processes a footnote line describing an omission,
i.e. footnotes which contain the string ``om.``.
The textual variation MUST include only only two witnesses,
hence omissions with two witnesses are not allowed since it would make
no sense for both witnesses to omit the same text. Therefore
the following should be true:
1. The footnote line contains one colon character.
2. The footnote line doesn't contain commas.
The first input argument must be the footnote line with the following
stripped from the start and end of the string:
1. All whitespace
2. ``*n*`` (where n is the footnote number) from the start of
the string
3. ``.`` character from the end of the string
The footnote is expected to contain a single ':' character and have the
following format:
1. The footnote line before the ':' character is a string of witness
text, followed by the ']' character, followed by a single witness
code.
2. The footnote line after the ':' character contains an 'om.' followed
by a single witness code.
The second input argument should be a list containing
the apparatus XML, this function will add XML to this list.
The third input argument is the string defining a unit of offset in
the XML, this defaults to four space characters.
It is intended this function is called by _footnotes()
for omission footnotes.
"""
reason = None
wits1 = None
corrections = None
# Split the footnote
try:
# Split to get the text and remove the space
_tmp = self.footnote.split(']')
if len(_tmp) != 2 or _tmp[1] == '':
raise FootnotesException
text = _tmp[0].strip()
# Split around : to check if correxi or conieci
_tmp = _tmp[1].strip().split(':')
if _tmp[0] in ['correxi', 'conieci']:
reason = _tmp[0].strip()
_tmp = ':'.join(_tmp[1:])
else:
_tmp = ':'.join(_tmp)
_tmp = _tmp.split('om.')
wits2 = [i.strip() for i in _tmp[1].split(',')]
_tmp = _tmp[0].strip(' :').split(',')
if _tmp[0] != '':
_ttmp = _tmp[0].split()
wits1 = [i.strip() for i in [_ttmp[-1]] + _tmp[1:]]
corrections = ' '.join(_ttmp[:-1])
self._d_footnote = {'reason': reason,
'text': text,
'witnesses': [wits1, wits2],
'corrections': corrections}
self._omission_xml()
except (IndexError, FootnotesException):
self.note_xml(self.footnote)
error = 'Omission error in footnote {}: {}'.format(self.n_footnote,
self.footnote)
logger.error(error)
def _omission_xml(self):
"""Method to create the XML portion related to footnote (TEI format)
"""
# try:
# Add the correxi or conieci if needed
if self._d_footnote['reason'] == 'correxi':
# Add text self.xml
self.xml.append(self.xml_oss + '<rdg>')
self.xml.append(self.xml_oss * 2 + '<choice>')
self.xml.append(self.xml_oss * 3 + '<corr>' +
self._d_footnote['text'] + '</corr>')
self.xml.append(self.xml_oss * 2 + '</choice>')
self.xml.append(self.xml_oss + '</rdg>')
elif self._d_footnote['reason'] == 'conieci':
# Add text self.xml
self.xml.append(self.xml_oss + '<rdg>')
self.xml.append(self.xml_oss * 2 + '<choice>')
self.xml.append(self.xml_oss * 3 + '<corr type="conjecture">' +
self._d_footnote['text'] + '</corr>')
self.xml.append(self.xml_oss * 2 + '</choice>')
self.xml.append(self.xml_oss + '</rdg>')
elif self._d_footnote['reason'] is not None:
raise FootnotesException
wits1, wits2 = self._d_footnote['witnesses']
if wits1 is not None:
for w in wits1:
_str = self.xml_oss + '<rdg wit="#' + w.strip() + '">'
self.wits.append(w)
if self._d_footnote['corrections']:
_str += self._d_footnote['corrections'] + '</rdg>'
else:
# _str += '\n' + self.xml_oss + '</rdg>'
_str += self._d_footnote['text'] + '</rdg>'
self.xml.append(_str)
for w in wits2:
_str = self.xml_oss + '<rdg wit="#' + w + '">'
self.wits.append(w)
_str += '\n' + self.xml_oss * 2 + '<gap reason="omission"/>'
_str += '\n' + self.xml_oss + '</rdg>'
self.xml.append(_str)
[docs] def correction(self, reason):
"""
This helper function processes a footnote line describing correxi, i.e.
corrections by the editor, these contain the string 'correxi'.
The first input argument must be the footnote line with the following
stripped from the start and end of the string:
1. All whitespace
2. ``*n*`` (where n is the footnote number) from the start of
the string
3. ``.`` character from the end of the string
The footnote is expected to contain at least one ``:`` character and
have the following format:
1. The footnote line before the first ``:`` character contains a string
of witness text, followed by a ``]`` character.
2. The footnote line after the ':' character has one of two formats:
a. multiple pairs of witness text + witness code, each pair
separated by a ``:`` character
b. a single witness text followed by a space and a list of comma
separated witness codes
The second input argument should be a list containing
the apparatus XML, this function will add XML to this list.
The third input argument is a string defining the unit of offset
for the XML, this defaults to four space characters.
It is intended this function is called by _footnotes()
for correxi footnotes.
"""
try:
# Split to get the text, the reason and remove the space
_tmp = self.footnote.split(']')
text = _tmp[0].strip()
_tmp = _tmp[1]
if reason in ['correxi', 'conieci']:
_tmp = _tmp.split(reason + ':')
_tmp = _tmp[1].strip()
elif reason == 'add':
_tmp = _tmp.split('add.')
_tmp = _tmp[1].strip()
# form: w1, w2: tttt w3, w4
c1 = _tmp.split(':')
d1 = c1[0].split(',')
e1 = d1[0].split()
corr1 = ' '.join(e1[:-1])
wits1 = [i.strip() for i in [d1[0].split()[-1]] + d1[1:]]
try:
d2 = c1[1].split(',')
e2 = d2[0].split()
corr2 = ' '.join(e2[:-1])
wits2 = [i.strip() for i in [e2[-1]] + d2[1:]]
except IndexError:
corr2 = ''
wits2 = []
self._d_footnote = {'reason': reason,
'text': text,
'witnesses': [wits1, wits2],
'corrections': [corr1, corr2]}
if self._d_footnote['reason'] == 'standard':
self._d_footnote['corrections'][0] = self._d_footnote['text']
self._correction_xml()
except (IndexError, FootnotesException):
self.note_xml(self.footnote)
error = 'Footnote error in footnote {}: {}'.format(self.n_footnote,
self.footnote)
logger.error(error)
def _correction_xml(self):
"""Method to create the XML portion related to footnote (TEI format)
Attributes
----------
xml : list
list of strings which contains the XML TEI for the footnote.
"""
# Add to the XML
if self._d_footnote['reason'] == 'add':
for i, wit in enumerate(self._d_footnote['witnesses']):
if wit:
for w in wit:
self.xml.append(self.xml_oss + '<rdg wit="#' +
w.strip() + '">')
self.xml.append(self.xml_oss * 2 +
'<add>' +
self._d_footnote['corrections'][i] +
'</add>')
self.note_xml('reason="add_scribe"')
self.xml.append(self.xml_oss + '</rdg>')
return
if (self._d_footnote['reason'] == 'correxi' or
self._d_footnote['reason'] == 'conieci'):
# Add text self.xml
self.xml.append(self.xml_oss + '<rdg>')
self.xml.append(self.xml_oss * 2 + '<choice>')
if self._d_footnote['reason'] == 'correxi':
self.xml.append(self.xml_oss * 3 + '<corr>' +
self._d_footnote['text'] +
'</corr>')
elif self._d_footnote['reason'] == 'conieci':
self.xml.append(self.xml_oss * 3 + '<corr type="conjecture">' +
self._d_footnote['text'] + '</corr>')
self.xml.append(self.xml_oss * 2 + '</choice>')
self.xml.append(self.xml_oss + '</rdg>')
for i in range(len(self._d_footnote['witnesses'])):
for w in self._d_footnote['witnesses'][i]:
self.wits.append(w)
self.xml.append(self.xml_oss +
'<rdg wit="#' + w.strip() + '">' +
self._d_footnote['corrections'][i] + '</rdg>')
[docs]class Footnotes(object):
"""Class to analyse and create the XML app file for the entire set of
footnotes.
Attributes
----------
footnotes : list, str, OrderedDict, dict
List which contains the whole set of footnote from the exegis
file.
"""
def __init__(self, footnotes=None):
if isinstance(footnotes, (list, str)):
self.footnotes = footnotes
self._dictionary()
elif isinstance(footnotes, (dict, OrderedDict)):
self.footnotes = footnotes
self.xml = []
self.wits = []
def _dictionary(self):
"""Create an ordered dictionary (OrderedDict object) with the footnotes
Returns
-------
dic : OrderedDict
contains the footnotes as an Ordered Dictionary.
Keys are the number of the footnote (integer) and value is
the footnote.
Raises
------
FootnotesException
if foot
"""
# Split the footnotes by lines (in theory one line per footnote)
# pylint: disable=locally-disabled, no-member
try:
if isinstance(self.footnotes, str) and self.footnotes != '':
_tmp = self.footnotes.splitlines()
elif isinstance(self.footnotes, list):
_tmp = self.footnotes
elif isinstance(self.footnotes, (dict, OrderedDict)):
return
_size = len(_tmp)
except UnboundLocalError:
error = ('Footnotes should be a non empty string, '
'a list, a dictionary or an OrderedDict '
'but is {}'.format(type(self.footnotes)))
logger.error(error)
raise FootnotesException
# Check that the number of footnote is in agreement
# with their numeration
try:
if not re.findall(str(_size), _tmp[-1])[0] == str(_size):
raise FootnotesException
except (IndexError, FootnotesException):
error = 'Number of footnotes {} not in agreement ' \
'with their numeration in the file'.format(_size)
logger.error(error)
raise FootnotesException
# Create the ordered dictionary and remove the '.'
_dic = OrderedDict()
for line in _tmp:
try:
pos_stars = [c.start() for c in re.finditer(r'\*',
line.strip())]
if len(pos_stars) < 2 or pos_stars[0] != 0:
raise FootnotesException
elif len(pos_stars) > 2:
warning = 'Problem in footnote: {}'.format(line)
logger.warning(warning)
logger.warning('There are a footnote reference inside '
'the footnote. This case is not treatable '
'by the actual version of the software')
key = line[1:pos_stars[1]]
value = line[pos_stars[1]+1:]
except FootnotesException:
error = 'There are a problem in footnote: {}'.format(line)
logger.error(error)
raise FootnotesException
# Remove space and '.'
_dic[int(key)] = value.strip('. ')
self.footnotes = _dic
[docs] def xml_app(self):
"""Method to create the XML add for the footnote
Returns
-------
xml_app : list
list which contains the lines with the XML related to the footnotes
"""
for n_footnote in self.footnotes.keys():
# Get the corresponding footnote (start at 1)
footnote_line = self.footnotes[n_footnote]
# Now process the footnote line - deal with each case individually
# to aid readability and make future additions easier
ft = Footnote(footnote_line, n_footnote, xml=[])
# Add initial XML to xml_app (for the apparatus XML file)
self.xml.append('<app from="#begin_fn' + str(n_footnote) +
'" to="#end_fn' + str(n_footnote) + '">')
ft.check_endnote()
# Now process the footnote
# Case 1 - omission
if 'om.' in ft.footnote:
ft.omission()
# Case 2 - addition
elif 'add.' in ft.footnote:
ft.correction('add')
# Case 3 - correxi
elif 'correxi' in ft.footnote:
ft.correction('correxi')
# Case4 - conieci
elif 'conieci' in ft.footnote:
ft.correction('conieci')
# Remaining case - standard variation
else:
ft.correction('standard')
self.xml += ft.xml
self.wits += ft.wits
# Close the XML
self.xml.append('</app>')
[docs] def save_xml(self, fname='xml_app.xml'):
"""Method to save the XML app string in a file
Parameters
----------
fname : str (optional)
name of the file where the XML app will be saved.
"""
with open(fname, 'w', encoding="utf-8") as f:
for s in self.xml:
f.write(s + '\n')