#!/usr/bin/env python
#****************************************************************************
# nodeformat.py, provides non-GUI base classes for node formating info
#
# TreeLine, an information storage program
# Copyright (C) 2005, Douglas W. Bell
#
# This is free software; you can redistribute it and/or modify it under the
# terms of the GNU General Public License, Version 2. This program is
# distributed in the hope that it will be useful, but WITTHOUT ANY WARRANTY.
#****************************************************************************
import re, copy, types, sys, os.path, stat, time
from xml.sax.saxutils import escape
if sys.platform != 'win32':
import pwd
from fieldformat import TextFormat, LongTextFormat, NumberFormat, \
ChoiceFormat, CombinationFormat, AutoChoiceFormat, \
DateFormat, TimeFormat, BooleanFormat, URLFormat, \
PathFormat, EmailFormat, InternalLinkFormat, \
ExecuteLinkFormat, PictureFormat, ParentFormat, \
AncestorFormat, ChildFormat, xslEscape
from gendate import GenDate
from gentime import GenTime
from conditional import Conditional
import treedoc
import globalref
class NodeFormat:
"""Stores node type field list and line formatting"""
lineAttrRe = re.compile('line\d+$')
fieldSplitRe = re.compile(r'({\*(?:\**|\?|!|&)[\w_\-.]+\*})', re.U)
fieldPartRe = re.compile(r'{\*(\**|\?|!|&)([\w_\-.]+)\*}', re.U)
def __init__(self, name, formatAttrs={}, defaultField=''):
self.name = name
self.fieldList = []
self.lineList = []
self.numEmptyFields = 0
self.numFullFields = 0
self.childType = formatAttrs.get(u'childtype', '')
self.genericType = formatAttrs.get(u'generic', '')
self.conditional = Conditional(formatAttrs.get(u'condition', ''))
self.sibPrefix = formatAttrs.get(u'sibprefix', '')
self.sibSuffix = formatAttrs.get(u'sibsuffix', '')
self.iconName = formatAttrs.get(u'icon', '')
self.refField = None
for key in formatAttrs.keys():
if NodeFormat.lineAttrRe.match(key):
self.insertLine(formatAttrs.get(key, ''), int(key[4:]))
if defaultField:
htmlAttrs = globalref.options.boolData('HtmlNewFields') and \
{'html': 'y'} or {}
self.addNewField(defaultField, htmlAttrs)
self.addLine(u'{*%s*}' % defaultField)
self.addLine(u'{*%s*}' % defaultField)
def duplicateSettings(self, otherFormat):
"""Set parameters to match other node format"""
self.name = otherFormat.name
self.fieldList = otherFormat.fieldList
self.lineList = otherFormat.lineList
self.childType = otherFormat.childType
self.genericType = otherFormat.genericType
self.conditional = otherFormat.conditional
self.sibPrefix = otherFormat.sibPrefix
self.sibSuffix = otherFormat.sibSuffix
self.iconName = otherFormat.iconName
self.refField = otherFormat.refField
def __cmp__(self, other):
"""Comparison for equality and sorting"""
return cmp(self.name, other.name)
def fieldNames(self):
"""Return list of names of fields"""
return [field.name for field in self.fieldList]
def lineFields(self):
"""Return list of first fieldname in each line"""
result = []
for line in self.lineList[1:]:
strippedLine = [part for part in line \
if type(part) not in types.StringTypes]
result.append(strippedLine and strippedLine[0].name or '')
return result
def findField(self, name):
"""Return field matching name or None"""
try:
return self.fieldList[self.fieldNames().index(name)]
except ValueError:
return None
def addNewField(self, name, attrs={}):
"""Add new field with type, format, extra text in attrs dict"""
field = globals()[attrs.get(u'type', 'Text') + 'Format'](name, attrs)
self.fieldList.append(field)
if attrs.get(u'ref', '').startswith('y'):
self.refField = field
elif not self.refField:
self.refField = self.fieldList[0]
sepName = field.sepName()
# replace field name with the field
self.lineList = [[part == sepName and field or part for part in line] \
for line in self.lineList]
def addFieldIfNew(self, name, attrs={}):
""""Add new field if it doesn't exist"""
if name not in self.fieldNames():
self.addNewField(name, attrs)
if self.genericType:
genericType = globalref.docRef.treeFormats.\
findFormat(self.genericType)
if genericType:
genericType.addNewField(name, attrs)
def addLine(self, text):
"""Add format line to end of lineList, line 0 is the title"""
newLine = self.parseLine(text)
if newLine:
self.lineList.append(newLine)
def insertLine(self, text, pos):
"""Change line at pos of lineList,
add blanks in earlier positions if req'd, line 0 is the title"""
newLine = self.parseLine(text)
if not newLine:
newLine = ['']
self.lineList.extend([''] * (pos - len(self.lineList) + 1))
self.lineList[pos] = newLine
def changeTitleLine(self, text):
"""Change the title format line"""
self.insertLine(text, 0)
def updateLineFields(self):
"""Re-find fields to update for any changes in the fieldList"""
self.lineList = [self.parseLine(line) for line in self.getLines()]
def parseLine(self, text):
"""Parse text format line, return list of field types and text"""
text = u' '.join(text.split())
lineList = filter(None, NodeFormat.fieldSplitRe.split(text))
newLine = []
for part in lineList:
fieldMatch = NodeFormat.fieldPartRe.match(part)
if fieldMatch:
modifier = fieldMatch.group(1)
fieldName = fieldMatch.group(2)
if not modifier:
field = self.findField(fieldName)
newLine.append(field and field or part)
elif modifier[0] == '*':
newLine.append(ParentFormat(fieldName, len(modifier)))
elif modifier == '?':
newLine.append(AncestorFormat(fieldName))
elif modifier == '&':
newLine.append(ChildFormat(fieldName))
else: # == '!' file field
field = globalref.docRef.fileInfoItem.nodeFormat.\
findField(fieldName)
newLine.append(field and field or part)
else:
newLine.append(part)
return newLine
def equalPrefix(self, other):
"""Return True if prefix and suffix are equivalent"""
if self is other:
return True
return self.sibPrefix.strip().upper() == \
other.sibPrefix.strip().upper() and \
self.sibSuffix.strip().upper() == \
other.sibSuffix.strip().upper()
def formatTitle(self, item):
"""Return string with formatted title data"""
if not self.lineList:
return ''
line = u''.join([self.fieldText(text, item, True) for text in \
self.lineList[0]])
return line.strip()
def formatText(self, item, skipEmpty=True, addPrefix=False, \
addSuffix=False, internal=False):
"""Return list of output strings from formatting data"""
tagExp = re.compile('.*(
|
|
|
)$')
result = []
for lineData in self.lineList[1:]:
self.numEmptyFields = 0
self.numFullFields = 0
line = u''.join([self.fieldText(text, item, False, internal) \
for text in lineData])
if self.numFullFields or not self.numEmptyFields or not skipEmpty:
result.append(line.strip()) # add if tags are not empty
else:
tagMatch = tagExp.match(line)
if tagMatch and result and globalref.docRef.formHtml:
# html tag at removed line end
result[-1] += tagMatch.group(1) # add to prev
if addPrefix and self.sibPrefix:
if result:
result[0] = self.sibPrefix + result[0]
else:
result = [self.sibPrefix]
if addSuffix and self.sibSuffix:
if result:
result[-1] += self.sibSuffix
else:
result = [self.sibSuffix]
return result
def formatAllTextLines(self, item):
"""Return a list of all text lines (title & output), add an empty
string if a line has empty tags"""
result = []
for lineData in self.lineList:
self.numEmptyFields = 0
self.numFullFields = 0
line = u''.join([self.fieldText(text, item, False) for text \
in lineData])
if self.numFullFields or not self.numEmptyFields:
result.append(line.strip()) # add if tags are not empty
else:
result.append('')
return result
def fieldText(self, field, item, titleMode=False, internal=False):
"""Return formatted text for field"""
if type(field) in types.StringTypes:
text = field
if not titleMode and not globalref.docRef.formHtml:
text = escape(text)
else:
text = field.outputText(item, titleMode, internal)
if text:
self.numFullFields += 1
else:
self.numEmptyFields += 1
return text
def setTitle(self, title, item, addUndo=False):
"""Set data based on title string, add undo item if addUndo,
return True if changed successfully"""
fields = []
pattern = u''
extraText = u''
for seg in self.lineList[0]:
if type(seg) in types.StringTypes:
pattern += re.escape(seg)
extraText += seg
elif seg.parentLevel:
text = self.fieldText(seg, item, True)
pattern += re.escape(text)
extraText += text
else:
fields.append(seg)
pattern += '(.*)'
match = re.match(pattern, title)
if not match and extraText.strip():
return False
if addUndo:
globalref.docRef.undoStore.addDataUndo(item, True)
if match:
for num, field in enumerate(fields):
item.data[field.name] = match.group(num+1)
else: # assign to 1st field if sep is only spaces
item.data[fields[0].name] = title
for field in fields[1:]:
item.data[field.name] = ''
if addUndo and globalref.pluginInterface:
globalref.pluginInterface.execCallback(globalref.pluginInterface.\
dataChangeCallbacks, item, \
fields)
return True
def formatXml(self):
"""Return text list for use in xml file"""
result = []
lines = self.getLines(True)
for i, line in enumerate(lines):
if line:
result.append(u'line%d="%s"' % \
(i, escape(line, treedoc.escDict)))
if self.childType:
result.append(u'childtype="%s"' % self.childType)
if self.genericType:
result.append(u'generic="%s"' % self.genericType)
if self.conditional:
result.append(u'condition="%s"' % escape(self.conditional.\
conditionText(), \
treedoc.escDict))
if self.sibPrefix:
result.append(u'sibprefix="%s"' % escape(self.sibPrefix, \
treedoc.escDict))
if self.sibSuffix:
result.append(u'sibsuffix="%s"' % escape(self.sibSuffix, \
treedoc.escDict))
if self.iconName:
result.append(u'icon="%s"' % self.iconName)
return result
def getLines(self, englishOnly=False):
"""Return text list of formatting lines"""
lines = [u''.join([self.fieldName(part, englishOnly) \
for part in line]) \
for line in self.lineList]
return lines and lines or ['']
def fieldName(self, field, englishOnly=False):
"""Return field name with separators or line text part"""
if type(field) in types.StringTypes:
return field
return field.sepName(englishOnly)
def addTableFields(self, headingList):
"""Set fields based on import table headings"""
headings = [self.correctFieldName(head) for head in headingList]
self.addLine(u'{*%s*}' % headings[0])
for text in headings:
self.addNewField(text)
self.addLine(u'{*%s*}' % text)
def xsltTemplate(self, indent, addAnchors=True):
"""Return list of lines for xslt template"""
tagExp = re.compile('.*(
|
|
|
)$')
brExp = re.compile('<[bB][rR][ /]*>')
brFormat = u'
'
hrExp = re.compile('<[hH][rR][ /]*>')
hrFormat = u'
'
lineList = copy.deepcopy(self.lineList[1:])
output = [u'', u'' % self.name]
if addAnchors:
name = self.refField.name
output.extend([u'' % name, \
u'' % name, \
u'', u''])
if self.sibPrefix:
output.append(xslEscape(self.sibPrefix))
for lineDataRaw in lineList:
lineData = [(type(field) not in types.StringTypes) and field or \
hrFormat.join(hrExp.split(brFormat.\
join(brExp.split(field)))) \
for field in lineDataRaw]
fields = [field for field in lineData if \
type(field) not in types.StringTypes]
if fields:
text = u'' % \
u' or '.join([field.xslTestText() for field in fields])
output.append(text)
tagMatch = tagExp.match(type(lineData[-1]) in \
types.StringTypes and \
lineData[-1] or '')
if tagMatch:
if len(tagMatch.group(1)) == len(lineData[-1]):
del lineData[-1]
else:
lineData[-1] = lineData[-1][:-len(tagMatch.group(1))]
line = u''.join([(type(text) in types.StringTypes) \
and xslEscape(text) or text.xslText() \
for text in lineData])
output.extend([line + brFormat, u''])
if tagMatch:
output.append(tagMatch.group(1))
else:
output.append(lineData[0] + brFormat)
if self.sibSuffix:
output.append(xslEscape(self.sibSuffix))
if globalref.docRef.spaceBetween:
output.append(u'
')
output.extend([u'' % indent, \
u'', u'
', \
u''])
return output
def fixImportedFormat(self, defaultFieldName):
"""Add default field if there are no fields, and
add title and output lines if missing"""
if not self.lineList:
if defaultFieldName in self.fieldNames():
self.addLine(u'{*%s*}' % defaultFieldName)
else:
self.addLine(self.name)
for fieldName in self.fieldNames():
self.addLine(u'%s="{*%s*}"' % (fieldName, fieldName))
if not self.fieldList:
self.addNewField(defaultFieldName)
def correctFieldName(self, name):
"""Return name with only alphanumerics, underscores, dashes and
periods allowed"""
illegalRe = re.compile(r'[^\w_\-.]', re.U)
name = illegalRe.sub('_', name.strip())
if not name:
return u'X'
if not name[0].isalpha() or name[:3].lower() == 'xml':
name = u'X' + name
return name
def removeField(self, field):
"""Remove all occurances of field from lines
return False if not found"""
cnt = 0
for lineData in self.lineList:
while field in lineData:
lineData.remove(field)
cnt += 1
self.lineList = filter(None, self.lineList)
while len(self.lineList) < 2:
self.lineList.append([''])
return cnt
def findLinkField(self):
"""Return most likely field containing a bookmark URL"""
availFields = [field for field in self.fieldList if \
field.typeName == u'URL']
if len(availFields) == 1:
return availFields[0]
if not availFields:
return None
for srchTerm in [globalref.docRef.linkFieldName, u'link', u'url', \
u'href']:
for field in availFields:
name = field.name.lower()
if field.name.lower() == srchTerm:
return field
for field in availFields:
if field.name.lower().find(srchTerm) >= 0:
return field
return availFields[0]
def findAutoChoiceFields(self):
"""Return a list of fields that need to have choices added"""
return [field for field in self.fieldList if field.autoAddChoices]
def setInitDefaultData(self, data):
"""Add initial default data from fields into supplied dict"""
for field in self.fieldList:
text = field.getInitDefault()
if text:
data[field.name] = text
def updateFromGeneric(self, treeFormats):
"""Update the field names and types in self (a derived type)
to match generic"""
if not self.genericType:
return
genericType = treeFormats.findFormat(self.genericType)
if not genericType:
print 'Warning - generic type %s not found' % self.genericType
self.genericType = ''
return
newFields = []
for field in genericType.fieldList:
matchingField = self.findField(field.name)
if matchingField and field.typeName == matchingField.typeName:
newFields.append(matchingField)
else:
newFields.append(copy.deepcopy(field))
self.fieldList = newFields
if self.refField not in self.fieldList:
self.refField = self.fieldList[0]
self.updateLineFields()
class FileInfoFormat(NodeFormat):
"""Stores file info type field list and header/footer formatting"""
name = u'INT_TL_FILE_DATA_FORM'
fileFieldName = N_('File_Name')
pathFieldName = N_('File_Path')
sizeFieldName = N_('File_Size')
dateFieldName = N_('File_Mod_Date')
timeFieldName = N_('File_Mod_Time')
ownerFieldName = N_('File_Owner')
pageNumFieldName = N_('Page_Number')
numPagesFieldName = N_('Number_of_Pages')
def __init__(self):
NodeFormat.__init__(self, FileInfoFormat.name)
self.addNewField(FileInfoFormat.fileFieldName)
self.addNewField(FileInfoFormat.pathFieldName)
self.addNewField(FileInfoFormat.sizeFieldName, {'type': 'Number'})
self.addNewField(FileInfoFormat.dateFieldName, {'type': 'Date'})
self.addNewField(FileInfoFormat.timeFieldName, {'type': 'Time'})
if sys.platform != 'win32':
self.addNewField(FileInfoFormat.ownerFieldName)
# page info only for print header:
self.addNewField(FileInfoFormat.pageNumFieldName)
self.fieldList[-1].showInDialog = False
self.addNewField(FileInfoFormat.numPagesFieldName)
self.fieldList[-1].showInDialog = False
for field in self.fieldList:
field.useFileInfo = True
def updateFileInfo(self):
"""Update data of file info item"""
fileName = globalref.docRef.fileName
item = globalref.docRef.fileInfoItem
try:
status = os.stat(fileName)
except:
item.data = {}
return
item.data[_(FileInfoFormat.fileFieldName)] = os.path.basename(fileName)
item.data[_(FileInfoFormat.pathFieldName)] = os.path.dirname(fileName)
item.data[_(FileInfoFormat.sizeFieldName)] = str(status[stat.ST_SIZE])
modTime = time.localtime(status[stat.ST_MTIME])
item.data[_(FileInfoFormat.dateFieldName)] = repr(GenDate(modTime))
item.data[_(FileInfoFormat.timeFieldName)] = repr(GenTime(modTime))
if sys.platform != 'win32':
try:
item.data[_(FileInfoFormat.ownerFieldName)] = \
pwd.getpwuid(status[stat.ST_UID])[0]
except KeyError:
item.data[_(FileInfoFormat.ownerFieldName)] = \
repr(status[stat.ST_UID])
def getHeaderFooter(self, header=True):
"""Return formatted text for the header or the footer"""
textLines = self.formatAllTextLines(globalref.docRef.fileInfoItem)
if header:
textLines = textLines[:3]
else:
textLines = textLines[3:6]
textLines.extend([''] * (3 - len(textLines)))
aligns = ['left', 'center', 'right']
result = ['']
count = 0
for text, align in zip(textLines, aligns):
if text:
result.append('| %s | ' % (align, text))
count += 1
result.append('
')
if count:
return '\n'.join(result)
return ''
def replaceListFormat(self):
"""Copy settings from the treeFormats list to here and
replace the list's file format object"""
listFormat = globalref.docRef.treeFormats.findFormat(self.name)
if listFormat:
for thisField, listField in zip(self.fieldList, \
listFormat.fieldList):
thisField.duplicateSettings(listField)
thisField.useFileInfo = True
self.lineList = listFormat.lineList
globalref.docRef.treeFormats.remove(listFormat)
globalref.docRef.treeFormats.append(self)
def translateFields(self):
"""Translate field names into the current language,
called after reading the english version from the file"""
for field in self.fieldList:
transName = _(str(field.name))
if transName != field.name:
field.enName = field.name
field.name = transName