#@+leo-ver=5-thin
#@+node:ekr.20031218072017.3018: * @file leoFileCommands.py
'''Classes relating to reading and writing .leo files.'''
#@+<< imports >>
#@+node:ekr.20050405141130: ** << imports >> (leoFileCommands)
import leo.core.leoGlobals as g
import leo.core.leoNodes as leoNodes
import binascii
import difflib
import time
if g.isPython3:
import io # Python 3.x
StringIO = io.StringIO
BytesIO = io.BytesIO
else:
import cStringIO # Python 2.x
StringIO = cStringIO.StringIO
import os
import pickle
import string
import sys
import tempfile
import zipfile
try:
# IronPython has problems with this.
import xml.sax
import xml.sax.saxutils
except Exception:
pass
import sqlite3
import hashlib
from contextlib import contextmanager
PRIVAREA = '---begin-private-area---'
#@-<< imports >>
#@+others
#@+node:ekr.20060918164811: ** Exception classes
[docs]class BadLeoFile(Exception):
def __init__(self, message):
self.message = message
Exception.__init__(self, message) # Init the base class.
def __str__(self):
return "Bad Leo File:" + self.message
[docs]class InvalidPaste(Exception):
pass
#@+node:ekr.20060919110638.19: ** class SaxContentHandler (XMLGenerator)
if sys.platform != 'cli':
[docs] class SaxContentHandler(xml.sax.saxutils.XMLGenerator):
'''A sax content handler class that reads Leo files.'''
#@+others
#@+node:ekr.20060919110638.20: *3* sax.__init__ & helpers
def __init__(self, c, fileName, silent, inClipboard):
'''Ctor for SaxContentHandler class.'''
self.c = c
self.fileName = fileName
self.silent = silent
self.inClipboard = inClipboard
out = sys.stdout if sys.stdout else g.fileLikeObject()
# Fix the exceedingly strange problem with Python 3.x and pythonw.exe.
# The sax ctor throws an exception in Python 3.x if sys.stdout is None.
# The workaround is use a disposable output stream in that case.
xml.sax.saxutils.XMLGenerator.__init__(self, out=out)
# Init the base class.
#@+<< define dispatch dict >>
#@+node:ekr.20060919110638.21: *4* << define dispatch dict >>
#@@nobeautify
# There is no need for an 'end' method if all info is carried in attributes.
self.dispatchDict = {
'change_string': (None,None),
'find_panel_settings': (None,None),
'find_string': (None,None),
'globals': (self.startGlobals,None),
'global_log_window_position': (None,None), # The position of the log window is no longer used.
'global_window_position': (self.startWinPos,None),
'leo_file': (None,None),
'leo_header': (self.startLeoHeader,None),
'preferences': (None,None),
't': (self.startTnode,self.endTnode),
'tnodes': (None,None),
'v': (self.startVnode,self.endVnode),
'vh': (self.startVH,self.endVH),
'vnodes': (self.startVnodes,None), # Causes window to appear.
}
#@-<< define dispatch dict >>
self.printElements = [] # 'all', 'v'
# Global attributes of the .leo file...
# self.body_outline_ratio = '0.5'
self.global_window_position = {}
self.encoding = 'utf-8'
# Semantics...
self.content = None
self.elementStack = []
self.errors = 0
self.tnxToListDict = {}
# Keys are tnx's (strings)
# Values are *lists* of SaxNodeClass objects.
self.level = 0
self.node = None
self.nodeList = [] # List of SaxNodeClass objects with the present VNode.
self.nodeStack = []
self.ratio = self.secondary_ratio = 0.5
self.rootNode = None # a sax node.
#@+node:ekr.20060919110638.29: *3* sax.Do nothing
[docs] def endElementNS(self, unused_name, unused_qname):
g.trace(unused_name)
[docs] def endDocument(self):
pass
[docs] def ignorableWhitespace(self, unused_whitespace):
pass
[docs] def skippedEntity(self, name):
g.trace(name)
[docs] def startElementNS(self, unused_name, unused_qname, unused_attrs):
g.trace(unused_name)
[docs] def startDocument(self):
pass
#@+node:ekr.20060919134313: *3* sax.Utils
#@+node:ekr.20060919110638.23: *4* attrsToList
[docs] def attrsToList(self, attrs):
'''Convert the attributes to a list of g.Bunches.
attrs: an Attributes item passed to startElement.'''
return [
g.Bunch(name=name, val=attrs.getValue(name))
for name in attrs.getNames()]
#@+node:ekr.20060919110638.26: *4* error
[docs] def error(self, message):
g.pr('\n\nXML error: %s\n' % (message))
self.errors += 1
#@+node:ekr.20060919110638.27: *4* inElement
[docs] def inElement(self, name):
return self.elementStack and name in self.elementStack
#@+node:ekr.20060919110638.28: *4* printStartElement
[docs] def printStartElement(self, name, attrs):
indent = '\t' * self.level or ''
if attrs.getLength() > 0:
g.pr('%s<%s %s>' % (
indent,
self.clean(name).strip(),
self.attrsToString(attrs, sep=' ')),
newline=False)
else:
g.pr('%s<%s>' % (
indent,
self.clean(name).strip()),
newline=False)
if name.lower() in ['v', 't', 'vnodes', 'tnodes',]:
g.pr('')
#@+node:ekr.20060919110638.24: *5* attrsToString
[docs] def attrsToString(self, attrs, sep='\n'):
'''Convert the attributes to a string.
attrs: an Attributes item passed to startElement.
sep: the separator charater between attributes.'''
result = [
'%s="%s"' % (bunch.name, bunch.val)
for bunch in self.attrsToList(attrs)
]
return sep.join(result)
#@+node:ekr.20060919110638.25: *5* clean
[docs] def clean(self, s):
return g.toEncodedString(s, "ascii")
#@+node:ekr.20060919110638.30: *3* sax.characters
[docs] def characters(self, content):
'''Handle the characters element.'''
if content and not g.isUnicode(content):
g.trace('Non-unicode content', repr(content))
content = g.toUnicode(content)
content = content.replace('\r', '')
if not content:
return
elementName = self.elementStack[-1].lower() if self.elementStack else '<no element name>'
if elementName in ('t', 'vh'):
self.content.append(content)
elif content.strip():
g.pr('unexpected content:', elementName, repr(content))
#@+node:ekr.20060919110638.31: *3* sax.endElement & helpers
[docs] def endElement(self, name):
'''Handle the end of any xml element.'''
name = name.lower()
if name in self.printElements or 'all' in self.printElements:
indent = '\t' * (self.level - 1) or ''
g.pr('%s</%s>' % (indent, self.clean(name).strip()))
data = self.dispatchDict.get(name)
if data is None:
g.trace('unknown end element', name)
else:
junk, func = data
if func:
func()
name2 = self.elementStack.pop()
assert name == name2
#@+node:ekr.20060919110638.32: *4* sax.endTnode
[docs] def endTnode(self):
'''Handle the end of a <tnode> element.'''
for sax_node in self.nodeList:
sax_node.bodyString = ''.join(self.content)
self.content = []
#@+node:ekr.20060919110638.33: *4* sax.endVnode
[docs] def endVnode(self):
'''Handle the end of a <vnode> element.'''
self.level -= 1
self.node = self.nodeStack.pop()
#@+node:ekr.20060919110638.34: *4* sax.endVH
[docs] def endVH(self):
'''Handle the end of a <vh> element.'''
if self.node:
self.node.headString = ''.join(self.content)
self.content = []
#@+node:ekr.20060919110638.45: *3* sax.getRootNode
[docs] def getRootNode(self):
if False:
self.rootNode.dump()
for child in self.rootNode.children:
child.dump()
return self.rootNode
#@+node:ekr.20061004054323: *3* sax.processingInstruction (stylesheet)
[docs] def processingInstruction(self, target, data):
'''
sax: handle an xml processing instruction.
We expect the target to be 'xml-stylesheet'.
'''
if target == 'xml-stylesheet':
# A strange hack. Don't set this for settings files.
# This looks like a strange sax interaction.
sfn = (self.c.shortFileName() or '').strip().lower()
if sfn.endswith('leosettings.leo') or sfn.endswith('myleosettings.leo'):
pass
else:
self.c.frame.stylesheet = data
# g.warning('','%s: %s' % (target,data))
else:
g.trace(target, data)
#@+node:ekr.20060919110638.35: *3* sax.startElement & helpers
[docs] def startElement(self, name, attrs):
name = name.lower()
if name in self.printElements or 'all' in self.printElements:
self.printStartElement(name, attrs)
self.elementStack.append(name)
data = self.dispatchDict.get(name)
if data is None:
if 1: g.trace('unknown start element', name)
else:
func, junk = data
if func:
func(attrs)
#@+node:ekr.20060919110638.36: *4* sax.getWindowPositionAttributes
[docs] def getWindowPositionAttributes(self, attrs):
c = self.c
d = {}
windowSize = g.app.loadManager.options.get('windowSize')
if windowSize is not None:
h, w = windowSize # checked in LM.scanOption.
d['height'] = h
d['width'] = w
for bunch in self.attrsToList(attrs):
name = bunch.name; val = bunch.val
if name in ('top', 'left'):
try:
d[name] = int(val)
except ValueError:
d[name] = 50 # A reasonable default.
elif g.enableDB and c.mFileName:
d = c.cacher.getCachedWindowPositionDict(c.mFileName)
if not d and c.fixed and c.fixedWindowPosition:
width, height, left, top = c.fixedWindowPosition
d = {'top': top, 'left': left, 'width': width, 'height': height}
if not d:
for bunch in self.attrsToList(attrs):
name = bunch.name; val = bunch.val
if name in ('top', 'left', 'width', 'height'):
try:
d[name] = int(val)
except ValueError:
d[name] = 100 # A reasonable default.
else:
g.trace(name, len(val))
return d # Assigned to self.global_window_position
#@+node:ekr.20060919110638.37: *4* sax.startGlobals
[docs] def startGlobals(self, attrs):
c = self.c
if self.inClipboard:
return
use_db = g.enableDB and c.mFileName
if use_db:
ratio, ratio2 = c.cacher.getCachedGlobalFileRatios()
self.ratio, self.secondary_ratio = ratio, ratio2
else:
try:
for bunch in self.attrsToList(attrs):
name = bunch.name; val = bunch.val
if name == 'body_outline_ratio':
self.ratio = float(val)
elif name == 'body_secondary_ratio':
self.secondary_ratio = float(val)
except Exception:
pass
#@+node:ekr.20060919110638.38: *4* sax.startWinPos
[docs] def startWinPos(self, attrs):
self.global_window_position = self.getWindowPositionAttributes(attrs)
#@+node:ekr.20060919110638.39: *4* sax.startLeoHeader
[docs] def startLeoHeader(self, unused_attrs):
self.tnxToListDict = {}
#@+node:ekr.20060919110638.40: *4* sax.startVH
[docs] def startVH(self, unused_attrs):
self.content = []
#@+node:ekr.20060919112118: *4* sax.startVnodes
[docs] def startVnodes(self, unused_attrs):
if self.inClipboard:
return # No need to do anything to the main window.
c = self.c; d = self.global_window_position
w = d.get('width', 700)
h = d.get('height', 500)
x = d.get('left', 50)
y = d.get('top', 50)
#
# Redraw the window before writing into it.
# Honor --minimized, --maximized or --fullscreen.
# 2013/10/25: do set the geometry for minimized windows.
if g.app.start_minimized:
c.frame.setTopGeometry(w, h, x, y)
elif not g.app.start_maximized and not g.app.start_fullscreen:
c.frame.setTopGeometry(w, h, x, y)
c.frame.deiconify()
c.frame.lift()
# Causes window to appear.
if c.frame.top:
c.frame.resizePanesToRatio(self.ratio, self.secondary_ratio)
if not self.silent and not g.unitTesting:
g.es("reading:", self.fileName)
#@+node:ekr.20060919110638.41: *4* sax.startTnode
[docs] def startTnode(self, attrs):
if not self.inElement('tnodes'):
self.error('<t> outside <tnodes>')
self.content = []
self.tnodeAttributes(attrs)
#@+node:ekr.20060919110638.42: *5* sax.tnodeAttributes (SaxContentHandler)
[docs] def tnodeAttributes(self, attrs):
# The VNode must have a tx attribute to associate content
# with the proper node.
node = self.node
self.nodeList = []
val = None
# Step one: find the tx attribute
for bunch in self.attrsToList(attrs):
name = bunch.name; val = bunch.val
if name == 'tx':
# 2010/02/03: This code formerly did something
# different when unit testing just to support a unit test.
# Hahaha. The unit test *caused* the bug!
self.nodeList = self.tnxToListDict.get(val, [])
break
if not self.nodeList:
self.error('Bad leo file: no node for <t tx=%s>' % (val))
return
# Step two: find all the other attributes:
for bunch in self.attrsToList(attrs):
name = bunch.name; val = bunch.val
if name != 'tx':
# Huge bug fix: 2009/7/1: was node == self.node.
for node in self.nodeList:
node.tnodeAttributes[name] = val
# if not self.nodeList:
# self.error('Bad leo file: no tx attribute for VNode')
#@+node:ekr.20060919110638.43: *4* sax.startVnode
[docs] def startVnode(self, attrs):
if not self.inElement('vnodes'):
self.error('<v> outside <vnodes>')
if self.rootNode:
parent = self.node
else:
self.rootNode = parent = SaxNodeClass() # The dummy parent node.
parent.headString = 'dummyNode'
self.node = SaxNodeClass()
parent.children.append(self.node)
self.vnodeAttributes(attrs)
self.nodeStack.append(parent)
return parent
#@+node:ekr.20060919110638.44: *5* sax.vnodeAttributes
# The native attributes of <v> elements are a, t, vtag, tnodeList,
# marks, expanded and descendentTnodeUnknownAttributes.
[docs] def vnodeAttributes(self, attrs):
node = self.node
for bunch in self.attrsToList(attrs):
name = bunch.name; val = bunch.val
if name == 't':
aList = self.tnxToListDict.get(val, [])
aList.append(self.node)
self.tnxToListDict[val] = aList
node.tnx = str(val) # nodeIndices.toString returns a string.
else:
node.attributes[name] = val
#@-others
#@+node:ekr.20060919110638.15: ** class SaxNodeClass
if sys.platform != 'cli':
[docs] class SaxNodeClass:
'''A class representing one <v> element.
Use getters to access the attributes, properties and rules of this mode.
'''
#@+others
#@+node:ekr.20060919110638.16: *3* node.__init__
def __init__(self):
self.attributes = {}
self.bodyString = ''
self.headString = ''
self.children = []
self.tnodeAttributes = {}
self.tnodeList = []
self.tnx = None
#@+node:ekr.20060919110638.17: *3* node.__str__ & __repr__
def __str__(self):
return '<v:%s %s %s>' % (id(self), self.headString, len(self.bodyString))
__repr__ = __str__
#@+node:ekr.20060919110638.18: *3* node.dump
[docs] def dump(self):
g.pr('\nnode: %s tnx: %s len(body): %d %s' % (
id(self), self.tnx, len(self.bodyString), self.headString))
g.pr('children:', g.listToString(self.children))
g.pr('attrs:', list(self.attributes.values()))
#@-others
#@+node:ekr.20160514120347.1: ** class FileCommands
[docs]class FileCommands(object):
"""A class creating the FileCommands subcommander."""
#@+others
#@+node:ekr.20090218115025.4: *3* fc.Birth
#@+node:ekr.20150509194827.1: *4* fc.cmd (decorator)
[docs] def cmd(name):
'''Command decorator for the FileCommands class.'''
# pylint: disable=no-self-argument
return g.new_cmd_decorator(name, ['c', 'fileCommands',])
#@+node:ekr.20031218072017.3019: *4* fc.ctor
def __init__(self, c):
'''Ctor for FileCommands class.'''
self.c = c
self.frame = c.frame
self.nativeTnodeAttributes = ('tx',)
self.nativeVnodeAttributes = (
'a',
'descendentTnodeUnknownAttributes',
'descendentVnodeUnknownAttributes', # New in Leo 4.5.
'expanded', 'marks', 't', 'tnodeList',
# 'vtag',
)
self.initIvars()
#@+node:ekr.20090218115025.5: *4* fc.initIvars
[docs] def initIvars(self):
'''Init ivars of the FileCommands class.'''
# General...
c = self.c
self.mFileName = ""
self.fileDate = -1
self.leo_file_encoding = c.config.new_leo_file_encoding
# The bin param doesn't exist in Python 2.3;
# the protocol param doesn't exist in earlier versions of Python.
# version = '.'.join([str(sys.version_info[i]) for i in (0,1)])
# For reading...
self.checking = False # True: checking only: do *not* alter the outline.
self.descendentExpandedList = []
self.descendentMarksList = []
self.forbiddenTnodes = []
self.descendentTnodeUaDictList = []
self.descendentVnodeUaDictList = []
self.ratio = 0.5
self.currentVnode = None
# For writing...
self.read_only = False
self.rootPosition = None
self.outputFile = None
self.openDirectory = None
self.putCount = 0
self.toString = False
self.usingClipboard = False
self.currentPosition = None
# New in 3.12...
self.copiedTree = None
self.gnxDict = {}
# keys are gnx strings as returned by canonicalTnodeIndex.
# Values are vnodes.
# 2011/12/10: This dict is never re-inited.
self.vnodesDict = {}
# keys are gnx strings; values are ignored
#@+node:ekr.20031218072017.3020: *3* fc.Reading
#@+node:ekr.20060919104836: *4* fc.Reading Top-level
#@+node:ekr.20070919133659.1: *5* fc.checkLeoFile
[docs] @cmd('check-leo-file')
def checkLeoFile(self, event=None):
'''The check-leo-file command.'''
fc = self; c = fc.c; p = c.p
# Put the body (minus the @nocolor) into the file buffer.
s = p.b; tag = '@nocolor\n'
if s.startswith(tag): s = s[len(tag):]
# Do a trial read.
self.checking = True
self.initReadIvars()
c.loading = True # disable c.changed
try:
try:
theFile = g.app.loadManager.openLeoOrZipFile(c.mFileName)
self.readSaxFile(
theFile, fileName='check-leo-file',
silent=False, inClipboard=False, reassignIndices=False)
g.blue('check-leo-file passed')
except Exception:
junk, message, junk = sys.exc_info()
# g.es_exception()
g.error('check-leo-file failed:', str(message))
finally:
self.checking = False
c.loading = False # reenable c.changed
#@+node:vitalije.20180304190953.1: *5* fc.getVnodeFromClipboard
[docs] def getVnodeFromClipboard(self, s):
c = self.c
self.initReadIvars()
# Save the hidden root's children.
children = c.hiddenRootNode.children
oldGnxDict = self.gnxDict
self.gnxDict = {}
self.usingClipboard = True
try:
# This encoding must match the encoding used in putLeoOutline.
s = g.toEncodedString(s, self.leo_file_encoding, reportErrors=True)
# readSaxFile modifies the hidden root.
v = self.readSaxFile(
theFile=None, fileName='<clipboard>',
silent=True, # don't tell about stylesheet elements.
inClipboard=True, reassignIndices=True, s=s)
if not v:
return g.es("the clipboard is not valid ", color="blue")
finally:
self.usingClipboard = False
self.gnxDict = oldGnxDict
# Restore the hidden root's children
c.hiddenRootNode.children = children
# Unlink v from the hidden root.
v.parents.remove(c.hiddenRootNode)
return v
[docs] def getPosFromClipboard(self, s):
v = self.getVnodeFromClipboard(s)
return leoNodes.Position(v)
#@+node:ekr.20031218072017.1559: *5* fc.getLeoOutlineFromClipboard & helpers
[docs] def getLeoOutlineFromClipboard(self, s, reassignIndices=True):
'''Read a Leo outline from string s in clipboard format.'''
c = self.c
current = c.p
if not current:
g.trace('no c.p')
return None
check = not reassignIndices
self.initReadIvars()
# Save the hidden root's children.
children = c.hiddenRootNode.children
#
# Save and clear gnxDict.
# This ensures that new indices will be used for all nodes.
if reassignIndices:
oldGnxDict = self.gnxDict
self.gnxDict = {}
else:
# All pasted nodes should already have unique gnx's.
ni = g.app.nodeIndices
for v in c.all_unique_nodes():
ni.check_gnx(c, v.fileIndex, v)
self.usingClipboard = True
try:
# This encoding must match the encoding used in putLeoOutline.
s = g.toEncodedString(s, self.leo_file_encoding, reportErrors=True)
# readSaxFile modifies the hidden root.
v = self.readSaxFile(
theFile=None, fileName='<clipboard>',
silent=True, # don't tell about stylesheet elements.
inClipboard=True, reassignIndices=reassignIndices, s=s)
if not v:
return g.es("the clipboard is not valid ", color="blue")
finally:
self.usingClipboard = False
# Restore the hidden root's children
c.hiddenRootNode.children = children
# Unlink v from the hidden root.
v.parents.remove(c.hiddenRootNode)
p = leoNodes.Position(v)
#
# Important: we must not adjust links when linking v
# into the outline. The read code has already done that.
if current.hasChildren() and current.isExpanded():
if check and not self.checkPaste(current, p):
return None
p._linkAsNthChild(current, 0, adjust=False)
else:
if check and not self.checkPaste(current.parent(), p):
return None
p._linkAfter(current, adjust=False)
if reassignIndices:
self.gnxDict = oldGnxDict
self.reassignAllIndices(p)
else:
# Fix #862: paste-retaining-clones can corrupt the outline.
self.linkChildrenToParents(p)
c.selectPosition(p)
self.initReadIvars()
return p
getLeoOutline = getLeoOutlineFromClipboard # for compatibility
#@+node:ekr.20080410115129.1: *6* fc.checkPaste
[docs] def checkPaste(self, parent, p):
'''Return True if p may be pasted as a child of parent.'''
if not parent:
return True
parents = list(parent.self_and_parents())
for p in p.self_and_subtree():
for z in parents:
if p.v == z.v:
g.warning('Invalid paste: nodes may not descend from themselves')
return False
return True
#@+node:ekr.20180424123010.1: *6* fc.linkChildrenToParents
[docs] def linkChildrenToParents(self, p):
'''
Populate the parent links in all children of p.
'''
for child in p.children():
if not child.v.parents:
child.v.parents.append(p.v)
self.linkChildrenToParents(child)
#@+node:ekr.20180425034856.1: *6* fc.reassignAllIndices
[docs] def reassignAllIndices(self, p):
'''Reassign all indices in p's subtree.'''
c = self.c
ni = g.app.nodeIndices
for p2 in p.self_and_subtree():
v = p2.v
index = ni.getNewIndex(v)
if 'gnx' in g.app.debug:
g.trace(c.shortFileName(), '**reassigning**', index, v)
#@+node:ekr.20031218072017.1553: *5* fc.getLeoFile & helpers
[docs] def getLeoFile(self,
theFile,
fileName,
readAtFileNodesFlag=True,
silent=False,
checkOpenFiles=True,
):
'''
Read a .leo file.
The caller should follow this with a call to c.redraw().
'''
fc, c = self, self.c
t1 = time.time()
c.setChanged(False) # May be set when reading @file nodes.
fc.warnOnReadOnlyFiles(fileName)
fc.checking = False
fc.mFileName = c.mFileName
fc.initReadIvars()
recoveryNode = None
try:
c.loading = True # disable c.changed
if not silent and checkOpenFiles:
# Don't check for open file when reverting.
g.app.checkForOpenFile(c, fileName)
ok = fc.getLeoFileHelper(theFile, fileName, silent)
# Read the .leo file and create the outline.
if ok:
fc.resolveTnodeLists()
# Do this before reading external files.
c.setFileTimeStamp(fileName)
if readAtFileNodesFlag:
# Redraw before reading the @file nodes so the screen isn't blank.
# This is important for big files like LeoPy.leo.
c.redraw()
recoveryNode = fc.readExternalFiles(fileName)
finally:
p = recoveryNode or c.p or c.lastTopLevel()
# lastTopLevel is a better fallback, imo.
# New in Leo 5.3. Delay the second redraw until idle time.
# This causes a slight flash, but corrects a hangnail.
def handler(timer, c=c, p=c.p):
c.initialFocusHelper()
c.redraw(p)
c.k.showStateAndMode()
c.outerUpdate()
timer.stop()
timer = g.IdleTime(handler, delay=0, tag='getLeoFile')
if timer:
timer.start()
else:
# Defensive code:
c.selectPosition(p)
c.initialFocusHelper()
c.k.showStateAndMode()
c.outerUpdate()
c.checkOutline()
# Must be called *after* ni.end_holding.
c.loading = False
# reenable c.changed
if not isinstance(theFile, sqlite3.Connection):
theFile.close()
# Fix bug https://bugs.launchpad.net/leo-editor/+bug/1208942
# Leo holding directory/file handles after file close?
if c.changed:
fc.propegateDirtyNodes()
c.setChanged(c.changed) # Refresh the changed marker.
fc.initReadIvars()
t2 = time.time()
g.es('read outline in %2.2f seconds' % (t2 - t1))
return ok, c.frame.ratio
#@+node:ekr.20090526081836.5841: *6* fc.getLeoFileHelper
[docs] def getLeoFileHelper(self, theFile, fileName, silent):
'''Read the .leo file and create the outline.'''
c, fc = self.c, self
try:
ok = True
v = fc.readSaxFile(
theFile,
fileName,
silent,
inClipboard=False,
reassignIndices=False,
)
if v:
# readSaxFile sets c.hiddenRootNode.
pass
else:
# v is None for minimal .leo files.
v = leoNodes.VNode(context=c)
v.setHeadString('created root node')
p = leoNodes.Position(v)
p._linkAsRoot(oldRoot=None)
c.changed = False
except BadLeoFile:
junk, message, junk = sys.exc_info()
if not silent:
g.es_exception()
c.alert(fc.mFileName + " is not a valid Leo file: " + str(message))
ok = False
return ok
#@+node:ekr.20100205060712.8314: *6* fc.handleNodeConflicts
[docs] def handleNodeConflicts(self):
'''Create a 'Recovered Nodes' node for each entry in c.nodeConflictList.'''
c = self.c
if not c.nodeConflictList:
return None
if not c.make_node_conflicts_node:
s = 'suppressed %s node conflicts' % len(c.nodeConflictList)
g.es(s, color='red')
g.pr('\n' + s + '\n')
return None
# Create the 'Recovered Nodes' node.
last = c.lastTopLevel()
root = last.insertAfter()
root.setHeadString('Recovered Nodes')
root.expand()
# For each conflict, create one child and two grandchildren.
for bunch in c.nodeConflictList:
tag = bunch.get('tag') or ''
gnx = bunch.get('gnx') or ''
fn = bunch.get('fileName') or ''
b1, h1 = bunch.get('b_old'), bunch.get('h_old')
b2, h2 = bunch.get('b_new'), bunch.get('h_new')
root_v = bunch.get('root_v') or ''
child = root.insertAsLastChild()
h = 'Recovered node "%s" from %s' % (h1, g.shortFileName(fn))
child.setHeadString(h)
if b1 == b2:
lines = [
'Headline changed...'
'%s gnx: %s root: %r' % (tag, gnx, root_v and root.v),
'old headline: %s' % (h1),
'new headline: %s' % (h2),
]
child.setBodyString('\n'.join(lines))
else:
line1 = '%s gnx: %s root: %r\nDiff...\n' % (tag, gnx, root_v and root.v)
d = difflib.Differ().compare(g.splitLines(b1), g.splitLines(b2))
# 2017/06/19: reverse comparison order.
diffLines = [z for z in d]
lines = [line1]
lines.extend(diffLines)
# There is less need to show trailing newlines because
# we don't report changes involving only trailing newlines.
child.setBodyString(''.join(lines))
n1 = child.insertAsNthChild(0)
n2 = child.insertAsNthChild(1)
n1.setHeadString('old:' + h1)
n1.setBodyString(b1)
n2.setHeadString('new:' + h2)
n2.setBodyString(b2)
return root
#@+node:ekr.20100124110832.6212: *6* fc.propegateDirtyNodes
[docs] def propegateDirtyNodes(self):
fc = self; c = fc.c
aList = [z for z in c.all_positions() if z.isDirty()]
for p in aList:
p.setAllAncestorAtFileNodesDirty()
#@+node:ekr.20120212220616.10537: *6* fc.readExternalFiles
[docs] def readExternalFiles(self, fileName):
'''Read all external files.'''
c, fc = self.c, self
c.atFileCommands.readAll(c.rootVnode(), force=False)
recoveryNode = fc.handleNodeConflicts()
# Do this after reading external files.
# The descendent nodes won't exist unless we have read
# the @thin nodes!
fc.restoreDescendentAttributes()
fc.setPositionsFromVnodes()
return recoveryNode
#@+node:ekr.20031218072017.1554: *6* fc.warnOnReadOnlyFiles
[docs] def warnOnReadOnlyFiles(self, fileName):
# os.access may not exist on all platforms.
try:
self.read_only = not os.access(fileName, os.W_OK)
except AttributeError:
self.read_only = False
except UnicodeError:
self.read_only = False
if self.read_only and not g.unitTesting:
g.error("read only:", fileName)
#@+node:ekr.20031218072017.3029: *5* fc.readAtFileNodes
[docs] def readAtFileNodes(self):
c = self.c; p = c.p
c.endEditing()
c.atFileCommands.readAll(p, force=True)
c.redraw()
# Force an update of the body pane.
c.setBodyString(p, p.b)
c.frame.body.onBodyChanged(undoType=None)
#@+node:ekr.20031218072017.2297: *5* fc.openLeoFile
[docs] def openLeoFile(self, theFile, fileName, readAtFileNodesFlag=True, silent=False):
'''Open a Leo file.'''
c, frame = self.c, self.c.frame
# Set c.openDirectory
theDir = g.os_path_dirname(fileName)
if theDir:
c.openDirectory = c.frame.openDirectory = theDir
# Get the file.
ok, ratio = self.getLeoFile(
theFile, fileName,
readAtFileNodesFlag=readAtFileNodesFlag,
silent=silent,
)
if ok:
frame.resizePanesToRatio(ratio, frame.secondary_ratio)
return ok
#@+node:ekr.20031218072017.3030: *5* fc.readOutlineOnly
[docs] def readOutlineOnly(self, theFile, fileName):
c = self.c
#@+<< Set the default directory >>
#@+node:ekr.20071211134300: *6* << Set the default directory >> (fc.readOutlineOnly)
#@+at
# The most natural default directory is the directory containing the .leo file
# that we are about to open. If the user has specified the "Default Directory"
# preference that will over-ride what we are about to set.
#@@c
theDir = g.os_path_dirname(fileName)
if theDir:
c.openDirectory = c.frame.openDirectory = theDir
#@-<< Set the default directory >>
ok, ratio = self.getLeoFile(theFile, fileName, readAtFileNodesFlag=False)
c.redraw()
c.frame.deiconify()
junk, junk, secondary_ratio = self.frame.initialRatios()
c.frame.resizePanesToRatio(ratio, secondary_ratio)
return ok
#@+node:vitalije.20170831144643.1: *5* fc.updateFromRefFile
[docs] def updateFromRefFile(self):
'''Updates public part of outline from the specified file.'''
fc = self; c = self.c
#@+others
#@+node:vitalije.20170831144827.2: *6* get_ref_filename
def get_ref_filename():
for v in priv_vnodes():
return g.splitLines(v.b)[0].strip()
#@+node:vitalije.20170831144827.3: *6* createSaxChildren2
def createSaxChildren2(sax_node, parent_v):
children = []
for sax_child in sax_node.children:
tnx = sax_child.tnx
v = fc.gnxDict.get(tnx)
if v: # A clone.
fc.updateSaxClone(sax_child, parent_v, v)
else:
v = fc.createSaxVnode(sax_child, parent_v)
createSaxChildren2(sax_child, v)
children.append(v)
return children
#@+node:vitalije.20170831144827.4: *6* pub_vnodes
def pub_vnodes():
for v in c.hiddenRootNode.children:
if v.h == PRIVAREA:
break
yield v
#@+node:vitalije.20170831144827.5: *6* priv_vnodes
def priv_vnodes():
pub = True
for v in c.hiddenRootNode.children:
if v.h == PRIVAREA:
pub = False
if pub: continue
yield v
#@+node:vitalije.20170831144827.6: *6* pub_gnxes
def sub_gnxes(children):
for v in children:
yield v.gnx
for gnx in sub_gnxes(v.children):
yield gnx
def pub_gnxes():
return sub_gnxes(pub_vnodes())
def priv_gnxes():
return sub_gnxes(priv_vnodes())
#@+node:vitalije.20170831144827.7: *6* restore_priv
def restore_priv(prdata, topgnxes):
vnodes = []
for row in prdata:
(gnx,
h,
b,
children,
parents,
iconVal,
statusBits,
ua) = row
v = leoNodes.VNode(context=c, gnx=gnx)
v._headString = h
v._bodyString = b
v.children = children
v.parents = parents
v.iconVal = iconVal
v.statusBits = statusBits
v.u = ua
vnodes.append(v)
pv = lambda x: fc.gnxDict.get(x, c.hiddenRootNode)
for v in vnodes:
v.children = [pv(x) for x in v.children]
v.parents = [pv(x) for x in v.parents]
for gnx in topgnxes:
v = fc.gnxDict[gnx]
c.hiddenRootNode.children.append(v)
#@+node:vitalije.20170831144827.8: *6* priv_data
def priv_data(gnxes):
dbrow = lambda v:(
v.gnx,
v.h,
v.b,
[x.gnx for x in v.children],
[x.gnx for x in v.parents],
v.iconVal,
v.statusBits,
v.u
)
return tuple(dbrow(fc.gnxDict[x]) for x in gnxes)
#@+node:vitalije.20170831144827.9: *6* nosqlite_commander
@contextmanager
def nosqlite_commander(fname):
oldname = c.mFileName
conn = getattr(c, 'sqlite_connection', None)
c.sqlite_connection = None
c.mFileName = fname
yield c
if c.sqlite_connection:
c.sqlite_connection.close()
c.mFileName = oldname
c.sqlite_connection = conn
#@-others
pubgnxes = set(pub_gnxes())
privgnxes = set(priv_gnxes())
privnodes = priv_data(privgnxes - pubgnxes)
toppriv = [v.gnx for v in priv_vnodes()]
fname = get_ref_filename()
with nosqlite_commander(fname):
theFile = open(fname, 'rb')
fc.initIvars()
fc.getLeoFile(theFile, fname, checkOpenFiles=False)
restore_priv(privnodes, toppriv)
c.redraw()
#@+node:vitalije.20170831154734.1: *5* fc.setReferenceFile
[docs] def setReferenceFile(self, fileName):
c = self.c
for v in c.hiddenRootNode.children:
if v.h == PRIVAREA:
v.b = fileName
break
else:
v = c.rootPosition().insertBefore().v
v.h = PRIVAREA
v.b = fileName
c.redraw()
g.es('set reference file:', g.shortFileName(fileName))
#@+node:ekr.20060919133249: *4* fc.Reading Common
# Methods common to both the sax and non-sax code.
#@+node:ekr.20031218072017.2004: *5* fc.canonicalTnodeIndex
[docs] def canonicalTnodeIndex(self, index):
"""Convert Tnnn to nnn, leaving gnx's unchanged."""
# index might be Tnnn, nnn, or gnx.
if index is None:
g.trace('Can not happen: index is None')
return None
junk, theTime, junk = g.app.nodeIndices.scanGnx(index, 0)
if theTime is None: # A pre-4.1 file index.
if index[0] == "T":
index = index[1:]
return index
#@+node:ekr.20040701065235.1: *5* fc.getDescendentAttributes
[docs] def getDescendentAttributes(self, s, tag=""):
'''s is a list of gnx's, separated by commas from a <v> or <t> element.
Parses s into a list.
This is used to record marked and expanded nodes.
'''
gnxs = s.split(',')
result = [gnx for gnx in gnxs if len(gnx) > 0]
return result
#@+node:EKR.20040627114602: *5* fc.getDescendentUnknownAttributes
# Pre Leo 4.5 Only @thin vnodes had the descendentTnodeUnknownAttributes field.
# New in Leo 4.5: @thin & @shadow vnodes have descendentVnodeUnknownAttributes field.
[docs] def getDescendentUnknownAttributes(self, s, v=None):
'''Unhexlify and unpickle t/v.descendentUnknownAttribute field.'''
try:
# Changed in version 3.2: Accept only bytestring or bytearray objects as input.
s = g.toEncodedString(s) # 2011/02/22
bin = binascii.unhexlify(s)
# Throws a TypeError if val is not a hex string.
val = pickle.loads(bin)
return val
except Exception:
g.es_exception()
g.trace('Can not unpickle', type(s), v and v.h, s[: 40])
return None
#@+node:ekr.20060919142200.1: *5* fc.initReadIvars
[docs] def initReadIvars(self):
self.descendentTnodeUaDictList = []
self.descendentVnodeUaDictList = []
self.descendentExpandedList = []
self.descendentMarksList = []
# 2011/12/10: never re-init this dict.
# self.gnxDict = {}
self.c.nodeConflictList = [] # 2010/01/05
self.c.nodeConflictFileName = None # 2010/01/05
#@+node:EKR.20040627120120: *5* fc.restoreDescendentAttributes
[docs] def restoreDescendentAttributes(self):
c = self.c
for resultDict in self.descendentTnodeUaDictList:
for gnx in resultDict:
tref = self.canonicalTnodeIndex(gnx)
v = self.gnxDict.get(tref)
if v:
v.unknownAttributes = resultDict[gnx]
v._p_changed = 1
# New in Leo 4.5: keys are archivedPositions, values are attributes.
for root_v, resultDict in self.descendentVnodeUaDictList:
for key in resultDict:
v = self.resolveArchivedPosition(key, root_v)
if v:
v.unknownAttributes = resultDict[key]
v._p_changed = 1
marks = {}; expanded = {}
for gnx in self.descendentExpandedList:
tref = self.canonicalTnodeIndex(gnx)
v = self.gnxDict.get(gnx)
if v:
expanded[v] = v
for gnx in self.descendentMarksList:
tref = self.canonicalTnodeIndex(gnx)
v = self.gnxDict.get(gnx)
if v: marks[v] = v
if marks or expanded:
for p in c.all_unique_positions():
if marks.get(p.v):
p.v.initMarkedBit()
# This was the problem: was p.setMark.
# There was a big performance bug in the mark hook in the Node Navigator plugin.
if expanded.get(p.v):
p.expand()
#@+node:ekr.20060919104530: *4* fc.Reading Sax
#@+node:ekr.20090525144314.6526: *5* fc.cleanSaxInputString
# for i in range(32): print i,repr(chr(i))
#@+node:ekr.20060919110638.5: *5* fc.createSaxChildren & helpers
[docs] def createSaxChildren(self, sax_node, parent_v):
'''Create vnodes for all children in sax_node.children.'''
children = []
for sax_child in sax_node.children:
tnx = sax_child.tnx
v = self.gnxDict.get(tnx)
if v: # A clone. Don't look at the children.
self.updateSaxClone(sax_child, parent_v, v)
else:
v = self.createSaxVnode(sax_child, parent_v)
self.createSaxChildren(sax_child, v)
children.append(v)
parent_v.children = children
for child in children:
child.parents.append(parent_v)
return children
#@+node:ekr.20060919110638.7: *6* fc.createSaxVnode & helpers
[docs] def createSaxVnode(self, sax_node, parent_v):
'''Create a vnode, or use an existing vnode.'''
c = self.c
at = c.atFileCommands
#
# Fix #158: Corrupt .leo files cause Leo to hang.
# Explicitly test against None: tnx could be 0.
if sax_node.tnx is None:
gnx = None
else:
gnx = g.toUnicode(self.canonicalTnodeIndex(sax_node.tnx))
#
# Allocate and init a new vnode.
v = leoNodes.VNode(context=c, gnx=gnx)
v.setBodyString(sax_node.bodyString)
at.bodySetInited(v)
v.setHeadString(sax_node.headString)
self.handleVnodeSaxAttributes(sax_node, v)
self.handleTnodeSaxAttributes(sax_node, v)
return v
#@+node:ekr.20060919110638.8: *7* fc.handleTnodeSaxAttributes
[docs] def handleTnodeSaxAttributes(self, sax_node, v):
d = sax_node.tnodeAttributes
aDict = {}
for key in d:
val = g.toUnicode(d.get(key))
val2 = self.getSaxUa(key, val)
aDict[key] = val2
if aDict:
v.unknownAttributes = aDict
#@+node:ekr.20061004053644: *7* fc.handleVnodeSaxAttributes
[docs] def handleVnodeSaxAttributes(self, sax_node, v):
'''
The native attributes of <v> elements are a, t, vtag, tnodeList,
marks, expanded, and descendentTnode/VnodeUnknownAttributes.
'''
d = sax_node.attributes
s = d.get('a')
if s:
if 'M' in s: v.setMarked()
if 'E' in s: v.expand()
if 'O' in s: v.setOrphan()
if 'V' in s: self.currentVnode = v
s = d.get('tnodeList', '')
tnodeList = s and s.split(',')
if tnodeList:
# This tnodeList will be resolved later.
v.tempTnodeList = tnodeList
s = d.get('descendentTnodeUnknownAttributes')
if s:
aDict = self.getDescendentUnknownAttributes(s, v=v)
if aDict:
self.descendentTnodeUaDictList.append(aDict)
s = d.get('descendentVnodeUnknownAttributes')
if s:
aDict = self.getDescendentUnknownAttributes(s, v=v)
if aDict:
self.descendentVnodeUaDictList.append((v, aDict),)
s = d.get('expanded')
if s:
aList = self.getDescendentAttributes(s, tag="expanded")
self.descendentExpandedList.extend(aList)
s = d.get('marks')
if s:
aList = self.getDescendentAttributes(s, tag="marks")
self.descendentMarksList.extend(aList)
aDict = {}
for key in d:
if key in self.nativeVnodeAttributes:
pass # This is not a bug.
else:
val = d.get(key)
val2 = self.getSaxUa(key, val)
aDict[key] = val2
if aDict:
v.unknownAttributes = aDict
#@+node:ekr.20180424120245.1: *6* fc.updateSaxClone
[docs] def updateSaxClone(self, sax_node, parent_v, v):
'''
Update the body text of v. It overrides any previous body text.
'''
at = self.c.atFileCommands
b = sax_node.bodyString
if v.b != b:
v.setBodyString(b)
at.bodySetInited(v)
#
# New in Leo 5.7.2. Don't call these
# self.handleVnodeSaxAttributes(sax_node, v)
# self.handleTnodeSaxAttributes(sax_node, v)
#@+node:ekr.20060919110638.2: *5* fc.dumpSaxTree
[docs] def dumpSaxTree(self, root, dummy):
if not root:
g.pr('dumpSaxTree: empty tree')
return
if not dummy:
root.dump()
for child in root.children:
self.dumpSaxTree(child, dummy=False)
#@+node:tbrown.20140615093933.89639: *5* fc.bytes_to_unicode
[docs] def bytes_to_unicode(self, ob):
"""recursively convert bytes objects in strings / lists / dicts to str
objects, thanks to TNT
http://stackoverflow.com/questions/22840092/unpickling-data-from-python-2-with-unicode-strings-in-python-3
Needed for reading Python 2.7 pickles in Python 3.4 in getSaxUa()
"""
# pylint: disable=unidiomatic-typecheck
# This is simpler than using isinstance.
t = type(ob)
if t in (list, tuple):
l = [str(i, 'utf-8') if type(i) is bytes else i for i in ob]
l = [self.bytes_to_unicode(i) if type(i) in (list, tuple, dict) else i
for i in l]
ro = tuple(l) if t is tuple else l
elif t is dict:
byte_keys = [i for i in ob if type(i) is bytes]
for bk in byte_keys:
v = ob[bk]
del(ob[bk])
ob[str(bk, 'utf-8')] = v
for k in ob:
if type(ob[k]) is bytes:
ob[k] = str(ob[k], 'utf-8')
elif type(ob[k]) in (list, tuple, dict):
ob[k] = self.bytes_to_unicode(ob[k])
ro = ob
elif t is bytes: # TNB added this clause
ro = str(ob, 'utf-8')
else:
ro = ob
return ro
#@+node:ekr.20061003093021: *5* fc.getSaxUa
[docs] def getSaxUa(self, attr, val, kind=None): # Kind is for unit testing.
"""Parse an unknown attribute in a <v> or <t> element.
The unknown tag has been pickled and hexlify'd.
"""
try:
# val = str(val)
val = g.toEncodedString(val) # 2011/02/22.
except Exception:
g.es_print('unexpected exception converting hexlified string to string')
g.es_exception()
return None
# New in 4.3: leave string attributes starting with 'str_' alone.
if attr.startswith('str_'):
if g.isString(val) or g.isBytes(val):
return g.toUnicode(val)
# New in 4.3: convert attributes starting with 'b64_' using the base64 conversion.
if 0: # Not ready yet.
if attr.startswith('b64_'):
try: pass
except Exception: pass
try:
binString = binascii.unhexlify(val) # Throws a TypeError if val is not a hex string.
except Exception:
# Python 2.x throws TypeError
# Python 3.x throws binascii.Error
# Assume that Leo 4.1 wrote the attribute.
if g.unitTesting:
assert kind == 'raw', 'unit test failed: kind=' % repr(kind)
else:
g.trace('can not unhexlify %s=%s' % (attr, val))
return val
try:
# No change needed to support protocols.
val2 = pickle.loads(binString)
return val2
except(pickle.UnpicklingError, ImportError, AttributeError, ValueError, TypeError):
try:
# for python 2.7 in python 3.4
# pylint: disable=unexpected-keyword-arg
val2 = pickle.loads(binString, encoding='bytes')
val2 = self.bytes_to_unicode(val2)
return val2
except(pickle.UnpicklingError, ImportError, AttributeError, ValueError, TypeError):
g.trace('can not unpickle %s=%s' % (attr, val))
return val
#@+node:ekr.20060919110638.14: *5* fc.parse_leo_file
[docs] def parse_leo_file(self, theFile, inputFileName, silent, inClipboard, s=None):
c = self.c
# Fix #434: Potential bug in settings.
if not theFile and not s:
return None
try:
if g.isPython3:
if theFile:
# Use the open binary file, opened by the caller.
s = theFile.read() # isinstance(s, bytes)
s = self.cleanSaxInputString(s)
theFile = BytesIO(s)
else:
s = str(s, encoding='utf-8')
s = self.cleanSaxInputString(s)
theFile = StringIO(s)
else:
if theFile: s = theFile.read()
s = self.cleanSaxInputString(s)
theFile = cStringIO.StringIO(s)
parser = xml.sax.make_parser()
parser.setFeature(xml.sax.handler.feature_external_ges, 1)
# Include external general entities, esp. xml-stylesheet lines.
if 0: # Expat does not read external features.
parser.setFeature(xml.sax.handler.feature_external_pes, 1)
# Include all external parameter entities
# Hopefully the parser can figure out the encoding from the <?xml> element.
# It's very hard to do anything meaningful wih an exception.
handler = SaxContentHandler(c, inputFileName, silent, inClipboard)
parser.setContentHandler(handler)
parser.parse(theFile) # expat does not support parseString
sax_node = handler.getRootNode()
except Exception:
g.error('error parsing', inputFileName)
g.es_exception()
sax_node = None
return sax_node
#@+node:ekr.20060919110638.3: *5* fc.readSaxFile
[docs] def readSaxFile(self, theFile, fileName, silent, inClipboard, reassignIndices, s=None):
'''Read the entire .leo file using the sax parser.'''
dump = False and not g.unitTesting
fc = self; c = fc.c
if fileName.endswith('.db'):
return fc.retrieveVnodesFromDb(theFile) or fc.initNewDb(theFile)
#
# Pass one: create the intermediate nodes.
saxRoot = fc.parse_leo_file(theFile, fileName,
silent=silent, inClipboard=inClipboard, s=s)
if not saxRoot:
return None
#
# Pass two: create the tree of vnodes from the intermediate nodes.
if dump: fc.dumpSaxTree(saxRoot, dummy=True)
parent_v = c.hiddenRootNode
children = fc.createSaxChildren(saxRoot, parent_v)
assert c.hiddenRootNode.children == children
v = children[0] if children else None
return v
#@+node:ekr.20060919110638.11: *5* fc.resolveTnodeLists
[docs] def resolveTnodeLists(self):
'''
Called *before* reading external files.
'''
c = self.c
for p in c.all_unique_positions():
if hasattr(p.v, 'tempTnodeList'):
result = []
for tnx in p.v.tempTnodeList:
index = self.canonicalTnodeIndex(tnx)
# new gnxs:
index = g.toUnicode(index)
v = self.gnxDict.get(index)
if v:
result.append(v)
else:
g.trace('*** No VNode for %s' % tnx)
if result:
p.v.tnodeList = result
delattr(p.v, 'tempTnodeList')
#@+node:ekr.20080805132422.3: *5* fc.resolveArchivedPosition
[docs] def resolveArchivedPosition(self, archivedPosition, root_v):
'''
Return a VNode corresponding to the archived position relative to root
node root_v.
'''
def oops(message):
'''Give an error only if no file errors have been seen.'''
return None
try:
aList = [int(z) for z in archivedPosition.split('.')]
aList.reverse()
except Exception:
return oops('"%s"' % archivedPosition)
if not aList:
return oops('empty')
last_v = root_v
n = aList.pop()
if n != 0:
return oops('root index="%s"' % n)
while aList:
n = aList.pop()
children = last_v.children
if n < len(children):
last_v = children[n]
else:
return oops('bad index="%s", len(children)="%s"' % (n, len(children)))
return last_v
#@+node:vitalije.20170630152841.1: *5* fc.retrieveVnodesFromDb
[docs] def retrieveVnodesFromDb(self, conn):
'''Recreates tree from the data contained in table vnodes. This
method follows behavior of readSaxFile.'''
fc = self; c = fc.c
sql = '''select gnx, head,
body,
children,
parents,
iconVal,
statusBits,
ua from vnodes'''
vnodes = []
try:
for row in conn.execute(sql):
(gnx,
h,
b,
children,
parents,
iconVal,
statusBits,
ua) = row
try:
ua = pickle.loads(g.toEncodedString(ua))
except ValueError:
ua = None
v = leoNodes.VNode(context=c, gnx=gnx)
v._headString = h
v._bodyString = b
v.children = children.split()
v.parents = parents.split()
v.iconVal = iconVal
v.statusBits = statusBits
v.u = ua
vnodes.append(v)
except sqlite3.Error as er:
if er.args[0].find('no such table') < 0:
# there was an error raised but it is not the one we expect
g.internalError(er)
# there is no vnodes table
return None
rootChildren = [x for x in vnodes if 'hidden-root-vnode-gnx' in x.parents]
if not rootChildren:
g.trace('there should be at least one top level node!')
return None
findNode = lambda x: fc.gnxDict.get(x, c.hiddenRootNode)
# let us replace every gnx with the corresponding vnode
for v in vnodes:
v.children = [findNode(x) for x in v.children]
v.parents = [findNode(x) for x in v.parents]
c.hiddenRootNode.children = rootChildren
(w, h, x, y, r1, r2, encp) = fc.getWindowGeometryFromDb(conn)
c.frame.setTopGeometry(w, h, x, y, adjustSize=True)
c.frame.resizePanesToRatio(r1, r2)
p = fc.decodePosition(encp)
c.setCurrentPosition(p)
return rootChildren[0]
#@+node:vitalije.20170815162307.1: *6* fc.initNewDb
[docs] def initNewDb(self, conn):
''' Initializes tables and returns None'''
fc = self; c = self.c
v = leoNodes.VNode(context=c)
c.hiddenRootNode.children = [v]
(w, h, x, y, r1, r2, encp) = fc.getWindowGeometryFromDb(conn)
c.frame.setTopGeometry(w, h, x, y, adjustSize=True)
c.frame.resizePanesToRatio(r1, r2)
c.sqlite_connection = conn
fc.exportToSqlite(c.mFileName)
return v
#@+node:vitalije.20170630200802.1: *6* fc.getWindowGeometryFromDb
[docs] def getWindowGeometryFromDb(self, conn):
geom = (600, 400, 50, 50 , 0.5, 0.5, '')
keys = ( 'width', 'height', 'left', 'top',
'ratio', 'secondary_ratio',
'current_position')
try:
d = dict(conn.execute('''select * from extra_infos
where name in (?, ?, ?, ?, ?, ?, ?)''', keys).fetchall())
geom = (d.get(*x) for x in zip(keys, geom))
except sqlite3.OperationalError:
pass
return geom
#@+node:ekr.20060919110638.13: *5* fc.setPositionsFromVnodes & helper (sax read)
[docs] def setPositionsFromVnodes(self):
c, root = self.c, self.c.rootPosition()
if c.sqlite_connection:
# position is already selected
return
current, str_pos = None, None
use_db = g.enableDB and c.mFileName
if use_db:
str_pos = c.cacher.getCachedStringPosition()
if not str_pos:
d = root.v.u
if d: str_pos = d.get('str_leo_pos')
if str_pos:
current = self.archivedPositionToPosition(str_pos)
c.setCurrentPosition(current or c.rootPosition())
#@+node:ekr.20061006104837.1: *6* fc.archivedPositionToPosition
[docs] def archivedPositionToPosition(self, s):
c = self.c
s = g.toUnicode(s) # 2011/02/25
aList = s.split(',')
try:
aList = [int(z) for z in aList]
except Exception:
aList = None
if not aList: return None
p = c.rootPosition(); level = 0
while level < len(aList):
i = aList[level]
while i > 0:
if p.hasNext():
p.moveToNext()
i -= 1
else:
return None
level += 1
if level < len(aList):
p.moveToFirstChild()
return p
#@+node:ekr.20031218072017.3032: *3* fc.Writing
#@+node:ekr.20070413045221.2: *4* fc.Top-level
#@+node:ekr.20031218072017.1720: *5* fc.save
[docs] def save(self, fileName, silent=False):
c = self.c
p = c.p
# New in 4.2. Return ok flag so shutdown logic knows if all went well.
ok = g.doHook("save1", c=c, p=p, fileName=fileName)
if ok is None:
c.endEditing() # Set the current headline text.
self.setDefaultDirectoryForNewFiles(fileName)
c.cacher.save(fileName, changeName=True)
ok = c.checkFileTimeStamp(fileName)
if ok:
if c.sqlite_connection:
c.sqlite_connection.close()
c.sqlite_connection = None
ok = self.write_Leo_file(fileName, False) # outlineOnlyFlag
if ok:
if not silent:
self.putSavedMessage(fileName)
c.setChanged(False) # Clears all dirty bits.
if c.config.save_clears_undo_buffer:
g.es("clearing undo")
c.undoer.clearUndoState()
c.redraw_after_icons_changed()
g.doHook("save2", c=c, p=p, fileName=fileName)
return ok
#@+node:vitalije.20170831135146.1: *5* fc.save_ref
[docs] def save_ref(self):
'''Saves reference outline file'''
c = self.c
p = c.p
fc = self
#@+others
#@+node:vitalije.20170831135535.1: *6* putVnodes2
def putVnodes2():
"""Puts all <v> elements in the order in which they appear in the outline."""
c.clearAllVisited()
fc.put("<vnodes>\n")
# Make only one copy for all calls.
fc.currentPosition = c.p
fc.rootPosition = c.rootPosition()
fc.vnodesDict = {}
ref_fname = None
for p in c.rootPosition().self_and_siblings():
if p.h == PRIVAREA:
ref_fname = p.b.split('\n',1)[0].strip()
break
# New in Leo 4.4.2 b2 An optimization:
fc.putVnode(p, isIgnore=p.isAtIgnoreNode()) # Write the next top-level node.
fc.put("</vnodes>\n")
return ref_fname
#@+node:vitalije.20170831135447.1: *6* getPublicLeoFile
def getPublicLeoFile():
fc.outputFile = g.FileLikeObject()
fc.updateFixedStatus()
fc.putProlog()
fc.putHeader()
fc.putGlobals()
fc.putPrefs()
fc.putFindSettings()
fname = putVnodes2()
fc.putTnodes()
fc.putPostlog()
return fname, fc.outputFile.getvalue()
#@-others
c.endEditing()
for v in c.hiddenRootNode.children:
if v.h == PRIVAREA:
fileName = g.splitLines(v.b)[0].strip()
break
else:
fileName = c.mFileName
# New in 4.2. Return ok flag so shutdown logic knows if all went well.
ok = g.doHook("save1", c=c, p=p, fileName=fileName)
if ok is None:
fileName, content = getPublicLeoFile()
fileName = g.os_path_finalize_join(c.openDirectory, fileName)
with open(fileName, 'w') as out:
out.write(content)
g.es('updated reference file:',
g.shortFileName(fileName))
g.doHook("save2", c=c, p=p, fileName=fileName)
return ok
#@+node:ekr.20031218072017.3043: *5* fc.saveAs
[docs] def saveAs(self, fileName):
c = self.c
p = c.p
if not g.doHook("save1", c=c, p=p, fileName=fileName):
c.endEditing() # Set the current headline text.
if c.sqlite_connection:
c.sqlite_connection.close()
c.sqlite_connection = None
self.setDefaultDirectoryForNewFiles(fileName)
c.cacher.save(fileName, changeName=True)
# Disable path-changed messages in writeAllHelper.
c.ignoreChangedPaths = True
try:
if self.write_Leo_file(fileName, outlineOnlyFlag=False):
c.setChanged(False) # Clears all dirty bits.
self.putSavedMessage(fileName)
finally:
c.ignoreChangedPaths = True
c.redraw_after_icons_changed()
g.doHook("save2", c=c, p=p, fileName=fileName)
#@+node:ekr.20031218072017.3044: *5* fc.saveTo
[docs] def saveTo(self, fileName, silent=False):
c = self.c
p = c.p
if not g.doHook("save1", c=c, p=p, fileName=fileName):
c.endEditing() # Set the current headline text.
if c.sqlite_connection:
c.sqlite_connection.close()
c.sqlite_connection = None
self.setDefaultDirectoryForNewFiles(fileName)
c.cacher.save(fileName, changeName=False)
# Disable path-changed messages in writeAllHelper.
c.ignoreChangedPaths = True
try:
self.write_Leo_file(fileName, outlineOnlyFlag=False)
finally:
c.ignoreChangedPaths = False
if not silent:
self.putSavedMessage(fileName)
c.redraw_after_icons_changed()
g.doHook("save2", c=c, p=p, fileName=fileName)
#@+node:ekr.20070413061552: *5* fc.putSavedMessage
[docs] def putSavedMessage(self, fileName):
c = self.c
# #531: Optionally report timestamp...
if c.config.getBool('log_show_save_time', default=False):
format = c.config.getString('log_timestamp_format') or "%H:%M:%S"
timestamp = time.strftime(format) + ' '
else:
timestamp = ''
zipMark = '[zipped] ' if c.isZipped else ''
g.es("%ssaved: %s%s" % (timestamp, zipMark, g.shortFileName(fileName)))
#@+node:ekr.20050404190914.2: *4* fc.deleteFileWithMessage
[docs] def deleteFileWithMessage(self, fileName, unused_kind):
try:
os.remove(fileName)
except Exception:
if self.read_only:
g.error("read only")
if not g.unitTesting:
g.error("exception deleting backup file:", fileName)
g.es_exception(full=False)
return False
#@+node:ekr.20031218072017.1470: *4* fc.put & helpers
[docs] def put(self, s):
'''Put string s to self.outputFile. All output eventually comes here.'''
# Improved code: self.outputFile (a cStringIO object) always exists.
if s:
self.putCount += 1
if not g.isPython3:
s = g.toEncodedString(s, self.leo_file_encoding, reportErrors=True)
self.outputFile.write(s)
#@+node:ekr.20141020112451.18329: *5* put_dquote
[docs] def put_dquote(self):
self.put('"')
#@+node:ekr.20141020112451.18330: *5* put_dquoted_bool
[docs] def put_dquoted_bool(self, b):
if b: self.put('"1"')
else: self.put('"0"')
#@+node:ekr.20141020112451.18331: *5* put_flag
[docs] def put_flag(self, a, b):
if a:
self.put(" "); self.put(b); self.put('="1"')
#@+node:ekr.20141020112451.18332: *5* put_in_dquotes
[docs] def put_in_dquotes(self, a):
self.put('"')
if a: self.put(a) # will always be True if we use backquotes.
else: self.put('0')
self.put('"')
#@+node:ekr.20141020112451.18333: *5* put_nl
[docs] def put_nl(self):
self.put("\n")
#@+node:ekr.20141020112451.18334: *5* put_tab
[docs] def put_tab(self):
self.put("\t")
#@+node:ekr.20141020112451.18335: *5* put_tabs
[docs] def put_tabs(self, n):
while n > 0:
self.put("\t")
n -= 1
#@+node:ekr.20031218072017.1971: *4* fc.putClipboardHeader
[docs] def putClipboardHeader(self):
# Put the minimum header for sax.
self.put('<leo_header file_format="2"/>\n')
#@+node:ekr.20040324080819.1: *4* fc.putLeoFile & helpers
[docs] def putLeoFile(self):
self.updateFixedStatus()
self.putProlog()
self.putHeader()
self.putGlobals()
self.putPrefs()
self.putFindSettings()
#start = g.getTime()
self.putVnodes()
#start = g.printDiffTime("vnodes ",start)
self.putTnodes()
#start = g.printDiffTime("tnodes ",start)
self.putPostlog()
#@+node:ekr.20031218072017.3035: *5* fc.putFindSettings
[docs] def putFindSettings(self):
# New in 4.3: These settings never get written to the .leo file.
self.put("<find_panel_settings/>")
self.put_nl()
#@+node:ekr.20031218072017.3037: *5* fc.putGlobals
# Changed for Leo 4.0.
[docs] def putGlobals(self):
c = self.c
use_db = g.enableDB and c.mFileName
if use_db:
c.cacher.setCachedGlobalsElement(c.mFileName)
# Always put positions, to trigger sax methods.
self.put("<globals")
#@+<< put the body/outline ratios >>
#@+node:ekr.20031218072017.3038: *6* << put the body/outline ratios >>
self.put(" body_outline_ratio=")
self.put_in_dquotes("0.5" if c.fixed or use_db else "%1.2f" % (
c.frame.ratio))
self.put(" body_secondary_ratio=")
self.put_in_dquotes("0.5" if c.fixed or use_db else "%1.2f" % (
c.frame.secondary_ratio))
#@-<< put the body/outline ratios >>
self.put(">"); self.put_nl()
#@+<< put the position of this frame >>
#@+node:ekr.20031218072017.3039: *6* << put the position of this frame >>
# New in Leo 4.5: support fixed .leo files.
if c.fixed or use_db:
width, height, left, top = 700, 500, 50, 50
# Put fixed, immutable, reasonable defaults.
# Leo 4.5 and later will ignore these when reading.
# These should be reasonable defaults so that the
# file will be opened properly by older versions
# of Leo that do not support fixed .leo files.
else:
width, height, left, top = c.frame.get_window_info()
self.put_tab()
self.put("<global_window_position")
self.put(" top="); self.put_in_dquotes(str(top))
self.put(" left="); self.put_in_dquotes(str(left))
self.put(" height="); self.put_in_dquotes(str(height))
self.put(" width="); self.put_in_dquotes(str(width))
self.put("/>"); self.put_nl()
#@-<< put the position of this frame >>
#@+<< put the position of the log window >>
#@+node:ekr.20031218072017.3040: *6* << put the position of the log window >>
top = left = height = width = 0 # no longer used
self.put_tab()
self.put("<global_log_window_position")
self.put(" top="); self.put_in_dquotes(str(top))
self.put(" left="); self.put_in_dquotes(str(left))
self.put(" height="); self.put_in_dquotes(str(height))
self.put(" width="); self.put_in_dquotes(str(width))
self.put("/>"); self.put_nl()
#@-<< put the position of the log window >>
self.put("</globals>"); self.put_nl()
#@+node:ekr.20031218072017.3041: *5* fc.putHeader
[docs] def putHeader(self):
tnodes = 0; clone_windows = 0 # Always zero in Leo2.
if 1: # For compatibility with versions before Leo 4.5.
self.put("<leo_header")
self.put(" file_format="); self.put_in_dquotes("2")
self.put(" tnodes="); self.put_in_dquotes(str(tnodes))
self.put(" max_tnode_index="); self.put_in_dquotes(str(0))
self.put(" clone_windows="); self.put_in_dquotes(str(clone_windows))
self.put("/>"); self.put_nl()
else:
self.put('<leo_header file_format="2"/>\n')
#@+node:ekr.20031218072017.3042: *5* fc.putPostlog
[docs] def putPostlog(self):
self.put("</leo_file>"); self.put_nl()
#@+node:ekr.20031218072017.2066: *5* fc.putPrefs
[docs] def putPrefs(self):
# New in 4.3: These settings never get written to the .leo file.
self.put("<preferences/>")
self.put_nl()
#@+node:ekr.20031218072017.1246: *5* fc.putProlog
[docs] def putProlog(self):
'''Put the prolog of the xml file.'''
tag = 'http://leoeditor.com/namespaces/leo-python-editor/1.1'
self.putXMLLine()
# Put "created by Leo" line.
self.put('<!-- Created by Leo: http://leoeditor.com/leo_toc.html -->')
self.put_nl()
self.putStyleSheetLine()
# Put the namespace
self.put('<leo_file xmlns:leo="%s" >' % tag)
self.put_nl()
#@+node:ekr.20031218072017.1248: *5* fc.putStyleSheetLine
[docs] def putStyleSheetLine(self):
'''
Put the xml stylesheet line.
Leo 5.3:
- Use only the stylesheet setting, ignoreing c.frame.stylesheet.
- Write no stylesheet element if there is no setting.
The old way made it almost impossible to delete stylesheet element.
'''
c = self.c
sheet = (c.config.getString('stylesheet') or '').strip()
# sheet2 = c.frame.stylesheet and c.frame.stylesheet.strip() or ''
# sheet = sheet or sheet2
if sheet:
s = '<?xml-stylesheet %s ?>' % sheet
self.put(s)
self.put_nl()
#@+node:ekr.20031218072017.1577: *5* fc.putTnode
[docs] def putTnode(self, v):
# Call put just once.
gnx = v.fileIndex
# pylint: disable=consider-using-ternary
ua = hasattr(v, 'unknownAttributes') and self.putUnknownAttributes(v) or ''
b = v.b
body = xml.sax.saxutils.escape(b) if b else ''
self.put('<t tx="%s"%s>%s</t>\n' % (gnx, ua, body))
#@+node:ekr.20031218072017.1575: *5* fc.putTnodes
[docs] def putTnodes(self):
"""Puts all tnodes as required for copy or save commands"""
self.put("<tnodes>\n")
self.putReferencedTnodes()
self.put("</tnodes>\n")
#@+node:ekr.20031218072017.1576: *6* fc.putReferencedTnodes
[docs] def putReferencedTnodes(self):
'''Put all referenced tnodes.'''
c = self.c
if self.usingClipboard: # write the current tree.
theIter = self.currentPosition.self_and_subtree()
else: # write everything
theIter = c.all_unique_positions()
# Populate tnodes
tnodes = {}
for p in theIter:
# Make *sure* the file index has the proper form.
# pylint: disable=unbalanced-tuple-unpacking
index = p.v.fileIndex
tnodes[index] = p.v
# Put all tnodes in index order.
for index in sorted(tnodes):
v = tnodes.get(index)
if v:
# Write only those tnodes whose vnodes were written.
# **Note**: @<file> trees are not written unless they contain clones.
if v.isWriteBit():
self.putTnode(v)
else:
g.trace('can not happen: no VNode for', repr(index))
# This prevents the file from being written.
raise BadLeoFile('no VNode for %s' % repr(index))
#@+node:ekr.20031218072017.1863: *5* fc.putVnode & helper
[docs] def putVnode(self, p, isIgnore=False):
"""Write a <v> element corresponding to a VNode."""
fc = self
v = p.v
isAuto = p.isAtAutoNode() and p.atAutoNodeName().strip()
isEdit = p.isAtEditNode() and p.atEditNodeName().strip() and not p.hasChildren()
# 2010/09/02: @edit nodes must not have children.
# If they do, the entire tree is written to the outline.
isFile = p.isAtFileNode()
isShadow = p.isAtShadowFileNode()
isThin = p.isAtThinFileNode()
isOrphan = p.isOrphan()
if not isIgnore:
isIgnore = p.isAtIgnoreNode()
# 2010/10/22: force writes of orphan @edit, @auto and @shadow trees.
if isIgnore: forceWrite = True # Always write full @ignore trees.
elif isAuto: forceWrite = isOrphan # Force write of orphan @auto trees.
elif isEdit: forceWrite = isOrphan # Force write of orphan @edit trees.
elif isFile: forceWrite = isOrphan # Force write of orphan @file trees.
elif isShadow: forceWrite = isOrphan # Force write of @shadow trees.
elif isThin: forceWrite = isOrphan # Force write of orphan @thin trees.
else: forceWrite = True # Write all other @<file> trees.
gnx = v.fileIndex
if forceWrite or self.usingClipboard:
v.setWriteBit() # 4.2: Indicate we wrote the body text.
attrs = fc.compute_attribute_bits(forceWrite, p)
# Write the node.
v_head = '<v t="%s"%s>' % (gnx, attrs)
if gnx in fc.vnodesDict:
fc.put(v_head + '</v>\n')
else:
fc.vnodesDict[gnx] = True
v_head += '<vh>%s</vh>' % (xml.sax.saxutils.escape(p.v.headString() or ''))
# New in 4.2: don't write child nodes of @file-thin trees
# (except when writing to clipboard)
if p.hasChildren() and (forceWrite or self.usingClipboard):
fc.put('%s\n' % v_head)
# This optimization eliminates all "recursive" copies.
p.moveToFirstChild()
while 1:
fc.putVnode(p, isIgnore)
if p.hasNext(): p.moveToNext()
else: break
p.moveToParent() # Restore p in the caller.
fc.put('</v>\n')
else:
fc.put('%s</v>\n' % v_head) # Call put only once.
#@+node:ekr.20031218072017.1865: *6* fc.compute_attribute_bits
[docs] def compute_attribute_bits(self, forceWrite, p):
'''Return the initial values of v's attributes.'''
c, v = self.c, p.v
attrs = []
# New in Leo 4.5: support fixed .leo files.
if not c.fixed:
bits = []
if v.isExpanded() and v.hasChildren() and c.putBitsFlag:
bits.append("E")
if v.isMarked():
bits.append("M")
if bits:
attrs.append(' a="%s"' % ''.join(bits))
# Put the archived *current* position in the *root* position's <v> element.
if p == self.rootPosition:
aList = [str(z) for z in self.currentPosition.archivedPosition()]
d = v.u
str_pos = ','.join(aList)
if d.get('str_leo_pos'):
del d['str_leo_pos']
# Don't write the current position if we can cache it.
if g.enableDB and c.mFileName:
c.cacher.setCachedStringPosition(str_pos)
elif c.fixed:
pass
else:
d['str_leo_pos'] = str_pos
v.u = d
elif hasattr(v, "unknownAttributes"):
d = v.unknownAttributes
if d and not c.fixed and d.get('str_leo_pos'):
del d['str_leo_pos']
v.unknownAttributes = d
# Append unKnownAttributes to attrs
if p.hasChildren() and not forceWrite and not self.usingClipboard:
# Fix #526: do this for @auto nodes as well.
attrs.append(self.putDescendentVnodeUas(p))
attrs.append(self.putDescendentAttributes(p))
return ''.join(attrs)
#@+node:ekr.20031218072017.1579: *5* fc.putVnodes
[docs] def putVnodes(self, p=None):
"""Puts all <v> elements in the order in which they appear in the outline."""
c = self.c
c.clearAllVisited()
self.put("<vnodes>\n")
# Make only one copy for all calls.
self.currentPosition = p or c.p
self.rootPosition = c.rootPosition()
self.vnodesDict = {}
if self.usingClipboard:
self.putVnode(self.currentPosition) # Write only current tree.
else:
for p in c.rootPosition().self_and_siblings():
# New in Leo 4.4.2 b2 An optimization:
self.putVnode(p, isIgnore=p.isAtIgnoreNode()) # Write the next top-level node.
self.put("</vnodes>\n")
#@+node:ekr.20031218072017.1247: *5* fc.putXMLLine
[docs] def putXMLLine(self):
'''Put the **properly encoded** <?xml> element.'''
# Use self.leo_file_encoding encoding.
self.put('%s"%s"%s\n' % (
g.app.prolog_prefix_string,
self.leo_file_encoding,
g.app.prolog_postfix_string))
#@+node:ekr.20031218072017.1573: *4* fc.putLeoOutline (to clipboard)
[docs] def putLeoOutline(self, p=None):
'''
Return a string, *not unicode*, encoded with self.leo_file_encoding,
suitable for pasting to the clipboard.
'''
p = p or self.c.p
self.outputFile = g.FileLikeObject()
self.usingClipboard = True
self.putProlog()
self.putClipboardHeader()
self.putVnodes(p)
self.putTnodes()
self.putPostlog()
s = self.outputFile.getvalue()
self.outputFile = None
self.usingClipboard = False
return s
#@+node:ekr.20031218072017.3046: *4* fc.write_Leo_file & helpers
[docs] def write_Leo_file(self, fileName, outlineOnlyFlag, toString=False, toOPML=False):
'''Write the .leo file.'''
c, fc = self.c, self
structure_errors = c.checkOutline()
if structure_errors:
g.error('Major structural errors! outline not written')
return False
if not outlineOnlyFlag or toOPML:
g.app.recentFilesManager.writeRecentFilesFile(c)
fc.writeAllAtFileNodesHelper() # Ignore any errors.
if fc.isReadOnly(fileName):
return False
if g.SQLITE and fileName and fileName.endswith('.db'):
return fc.exportToSqlite(fileName)
try:
fc.putCount = 0
fc.toString = toString
if toString:
ok = fc.writeToStringHelper(fileName)
else:
ok = fc.writeToFileHelper(fileName, toOPML)
finally:
fc.outputFile = None
fc.toString = False
return ok
write_LEO_file = write_Leo_file # For compatibility with old plugins.
#@+node:ekr.20040324080359.1: *5* fc.isReadOnly
[docs] def isReadOnly(self, fileName):
# self.read_only is not valid for Save As and Save To commands.
if g.os_path_exists(fileName):
try:
if not os.access(fileName, os.W_OK):
g.error("can not write: read only:", fileName)
return True
except Exception:
pass # os.access() may not exist on all platforms.
return False
#@+node:ekr.20100119145629.6114: *5* fc.writeAllAtFileNodesHelper
[docs] def writeAllAtFileNodesHelper(self):
'''Write all @<file> nodes and set orphan bits.'''
c = self.c
try:
# 2010/01/19: Do *not* signal failure here.
# This allows Leo to quit properly.
c.atFileCommands.writeAll()
return True
except Exception:
# Work around bug 1260415: https://bugs.launchpad.net/leo-editor/+bug/1260415
g.es_error("exception writing external files")
g.es_exception()
g.es('Internal error writing one or more external files.', color='red')
g.es('Please report this error to:', color='blue')
g.es('https://groups.google.com/forum/#!forum/leo-editor', color='blue')
g.es('All changes will be lost unless you', color='red')
g.es('can save each changed file.', color='red')
return False
#@+node:ekr.20100119145629.6111: *5* fc.writeToFileHelper & helpers
[docs] def writeToFileHelper(self, fileName, toOPML):
c = self.c; toZip = c.isZipped
ok, backupName = self.createBackupFile(fileName)
if not ok: return False
fileName, theActualFile = self.createActualFile(fileName, toOPML, toZip)
if not theActualFile: return False
self.mFileName = fileName
self.outputFile = StringIO() # Always write to a string.
try:
if toOPML:
if hasattr(c, 'opmlController'):
c.opmlController.putToOPML(owner=self)
else:
# This is not likely ever to be called.
g.trace('leoOPML plugin not active.')
else:
self.putLeoFile()
s = self.outputFile.getvalue()
g.app.write_Leo_file_string = s # 2010/01/19: always set this.
if toZip:
self.writeZipFile(s)
else:
if g.isPython3:
s = bytes(s, self.leo_file_encoding, 'replace')
theActualFile.write(s)
theActualFile.close()
c.setFileTimeStamp(fileName)
# raise AttributeError # To test handleWriteLeoFileException.
# Delete backup file.
if backupName and g.os_path_exists(backupName):
self.deleteFileWithMessage(backupName, 'backup')
return True
except Exception:
self.handleWriteLeoFileException(
fileName, backupName, theActualFile)
return False
#@+node:ekr.20100119145629.6106: *6* fc.createActualFile
[docs] def createActualFile(self, fileName, toOPML, toZip):
if toZip:
self.toString = True
theActualFile = None
else:
try:
# 2010/01/21: always write in binary mode.
theActualFile = open(fileName, 'wb')
except IOError:
g.es('can not open %s' % fileName)
g.es_exception()
theActualFile = None
return fileName, theActualFile
#@+node:ekr.20031218072017.3047: *6* fc.createBackupFile
[docs] def createBackupFile(self, fileName):
'''
Create a closed backup file and copy the file to it,
but only if the original file exists.
'''
if g.os_path_exists(fileName):
fd, backupName = tempfile.mkstemp(text=False)
f = open(fileName, 'rb') # rb is essential.
s = f.read()
f.close()
try:
try:
os.write(fd, s)
finally:
os.close(fd)
ok = True
except Exception:
g.error('exception creating backup file')
g.es_exception()
ok, backupName = False, None
if not ok and self.read_only:
g.error("read only")
else:
ok, backupName = True, None
return ok, backupName
#@+node:ekr.20100119145629.6108: *6* fc.handleWriteLeoFileException
[docs] def handleWriteLeoFileException(self, fileName, backupName, theActualFile):
c = self.c
g.es("exception writing:", fileName)
g.es_exception(full=True)
if theActualFile:
theActualFile.close()
# Delete fileName.
if fileName and g.os_path_exists(fileName):
self.deleteFileWithMessage(fileName, '')
# Rename backupName to fileName.
if backupName and g.os_path_exists(backupName):
g.es("restoring", fileName, "from", backupName)
# No need to create directories when restoring.
g.utils_rename(c, backupName, fileName)
else:
g.error('backup file does not exist!', repr(backupName))
#@+node:ekr.20100119145629.6110: *5* fc.writeToStringHelper
[docs] def writeToStringHelper(self, fileName):
try:
self.mFileName = fileName
self.outputFile = StringIO()
self.putLeoFile()
s = self.outputFile.getvalue()
g.app.write_Leo_file_string = s
return True
except Exception:
g.es("exception writing:", fileName)
g.es_exception(full=True)
g.app.write_Leo_file_string = ''
return False
#@+node:ekr.20070412095520: *5* fc.writeZipFile
[docs] def writeZipFile(self, s):
# The name of the file in the archive.
contentsName = g.toEncodedString(
g.shortFileName(self.mFileName),
self.leo_file_encoding, reportErrors=True)
# The name of the archive itself.
fileName = g.toEncodedString(
self.mFileName,
self.leo_file_encoding, reportErrors=True)
# Write the archive.
theFile = zipfile.ZipFile(fileName, 'w', zipfile.ZIP_DEFLATED)
theFile.writestr(contentsName, s)
theFile.close()
#@+node:vitalije.20170630172118.1: *5* fc.exportToSqlite
[docs] def exportToSqlite(self, fileName):
'''Dump all vnodes to sqlite database. Returns True on success.'''
# fc = self
c = self.c; fc = self
if c.sqlite_connection is None:
c.sqlite_connection = sqlite3.connect(fileName,
isolation_level='DEFERRED')
conn = c.sqlite_connection
def dump_u(v):
try:
s = pickle.dumps(v.u, protocol=1)
except pickle.PicklingError:
s = ''
g.trace('unpickleable value', repr(v.u))
return s
dbrow = lambda v:(
v.gnx,
v.h,
v.b,
' '.join(x.gnx for x in v.children),
' '.join(x.gnx for x in v.parents),
v.iconVal,
v.statusBits,
dump_u(v)
)
ok = False
try:
fc.prepareDbTables(conn)
fc.exportDbVersion(conn)
fc.exportVnodesToSqlite(conn, (dbrow(v) for v in c.all_unique_nodes()))
fc.exportGeomToSqlite(conn)
fc.exportHashesToSqlite(conn)
conn.commit()
ok = True
except sqlite3.Error as e:
g.internalError(e)
return ok
#@+node:vitalije.20170705075107.1: *6* fc.decodePosition
[docs] def decodePosition(self, s):
'''Creates position from its string representation encoded by fc.encodePosition.'''
fc = self
if not s:
return fc.c.rootPosition()
sep = g.u('<->')
comma = g.u(',')
stack = [x.split(comma) for x in s.split(sep)]
stack = [(fc.gnxDict[x], int(y)) for x,y in stack]
v, ci = stack[-1]
p = leoNodes.Position(v, ci, stack[:-1])
return p
#@+node:vitalije.20170705075117.1: *6* fc.encodePosition
[docs] def encodePosition(self, p):
'''New schema for encoding current position hopefully simplier one.'''
jn = g.u('<->')
mk = g.u('%s,%s')
res = [mk%(x.gnx, y) for x,y in p.stack]
res.append(mk%(p.gnx, p._childIndex))
return jn.join(res)
#@+node:vitalije.20170811130512.1: *6* fc.prepareDbTables
[docs] def prepareDbTables(self, conn):
conn.execute('''drop table if exists vnodes;''')
conn.execute('''
create table if not exists vnodes(
gnx primary key,
head,
body,
children,
parents,
iconVal,
statusBits,
ua);''')
conn.execute('''create table if not exists extra_infos(name primary key, value)''')
#@+node:vitalije.20170701161851.1: *6* fc.exportVnodesToSqlite
[docs] def exportVnodesToSqlite(self, conn, rows):
conn.executemany('''insert into vnodes
(gnx, head, body, children, parents,
iconVal, statusBits, ua)
values(?,?,?,?,?,?,?,?);''', rows)
#@+node:vitalije.20170701162052.1: *6* fc.exportGeomToSqlite
[docs] def exportGeomToSqlite(self, conn):
c = self.c
data = zip(
(
'width', 'height', 'left', 'top',
'ratio', 'secondary_ratio',
'current_position'
),
c.frame.get_window_info() +
(
c.frame.ratio, c.frame.secondary_ratio,
self.encodePosition(c.p)
)
)
conn.executemany('replace into extra_infos(name, value) values(?, ?)', data)
#@+node:vitalije.20170811130559.1: *6* fc.exportDbVersion
[docs] def exportDbVersion(self, conn):
conn.execute("replace into extra_infos(name, value) values('dbversion', ?)", ('1.0',))
#@+node:vitalije.20170701162204.1: *6* fc.exportHashesToSqlite
[docs] def exportHashesToSqlite(self, conn):
c = self.c
def md5(x):
try:
s = open(x, 'rb').read()
except Exception:
return ''
s = s.replace(b'\r\n', b'\n')
return hashlib.md5(s).hexdigest()
files = set()
p = c.rootPosition()
while p:
if p.isAtIgnoreNode():
p.moveToNodeAfterTree()
elif p.isAtAutoNode() or p.isAtFileNode():
fn = c.getNodeFileName(p)
files.add((fn, 'md5_'+p.gnx))
p.moveToNodeAfterTree()
else:
p.moveToThreadNext()
# pylint: disable=deprecated-lambda
conn.executemany(
'replace into extra_infos(name, value) values(?,?)',
map(lambda x:(x[1], md5(x[0])), files))
#@+node:ekr.20031218072017.2012: *4* fc.writeAtFileNodes
[docs] @cmd('write-at-file-nodes')
def writeAtFileNodes(self, event=None):
'''Write all @file nodes in the selected outline.'''
c = self.c
c.init_error_dialogs()
c.atFileCommands.writeAll(writeAtFileNodesFlag=True)
c.raise_error_dialogs(kind='write')
#@+node:ekr.20080801071227.5: *4* fc.writeAtShadowNodes
[docs] def writeAtShadowNodes(self, event=None):
'''Write all @file nodes in the selected outline.'''
c = self.c
c.init_error_dialogs()
c.atFileCommands.writeAll(writeAtFileNodesFlag=True)
c.raise_error_dialogs(kind='write')
#@+node:ekr.20031218072017.1666: *4* fc.writeDirtyAtFileNodes
[docs] @cmd('write-dirty-at-file-nodes')
def writeDirtyAtFileNodes(self, event=None):
'''Write all changed @file Nodes.'''
c = self.c
c.init_error_dialogs()
c.atFileCommands.writeAll(writeDirtyAtFileNodesFlag=True)
c.raise_error_dialogs(kind='write')
#@+node:ekr.20080801071227.6: *4* fc.writeDirtyAtShadowNodes
[docs] def writeDirtyAtShadowNodes(self, event=None):
'''Write all changed @shadow Nodes.'''
self.c.atFileCommands.writeDirtyAtShadowNodes()
#@+node:ekr.20031218072017.2013: *4* fc.writeMissingAtFileNodes
[docs] @cmd('write-missing-at-file-nodes')
def writeMissingAtFileNodes(self, event=None):
'''Write all @file nodes for which the corresponding external file does not exist.'''
c = self.c
if c.p:
c.atFileCommands.writeMissing(c.p)
#@+node:ekr.20031218072017.3050: *4* fc.writeOutlineOnly
[docs] @cmd('write-outline-only')
def writeOutlineOnly(self, event=None):
'''Write the entire outline without writing any derived files.'''
c = self.c
c.endEditing()
self.write_Leo_file(self.mFileName, outlineOnlyFlag=True)
g.blue('done')
#@+node:ekr.20080805114146.2: *3* fc.Utils
#@+node:ekr.20031218072017.1570: *4* fc.assignFileIndices & compactFileIndices
[docs] def assignFileIndices(self):
"""Assign a file index to all tnodes"""
pass # No longer needed: we assign indices as needed.
# Indices are now immutable, so there is no longer any difference between these two routines.
compactFileIndices = assignFileIndices
#@+node:ekr.20080805085257.1: *4* fc.createUaList
[docs] def createUaList(self, aList):
'''Given aList of pairs (p,torv), return a list of pairs (torv,d)
where d contains all picklable items of torv.unknownAttributes.'''
result = []
for p, torv in aList:
if isinstance(torv.unknownAttributes, dict):
# Create a new dict containing only entries that can be pickled.
d = dict(torv.unknownAttributes) # Copy the dict.
for key in d:
# Just see if val can be pickled. Suppress any error.
ok = self.pickle(torv=torv, val=d.get(key), tag=None)
if not ok:
del d[key]
g.warning("ignoring bad unknownAttributes key", key, "in", p.h)
if d:
result.append((torv, d),)
else:
g.warning("ignoring non-dictionary uA for", p)
return result
#@+node:ekr.20080805085257.2: *4* fc.pickle
[docs] def pickle(self, torv, val, tag):
'''Pickle val and return the hexlified result.'''
try:
s = pickle.dumps(val, protocol=1)
s2 = binascii.hexlify(s)
s3 = g.ue(s2, 'utf-8')
field = ' %s="%s"' % (tag, s3)
return field
except pickle.PicklingError:
if tag: # The caller will print the error if tag is None.
g.warning("ignoring non-pickleable value", val, "in", torv)
return ''
except Exception:
g.error("fc.pickle: unexpected exception in", torv)
g.es_exception()
return ''
#@+node:ekr.20040701065235.2: *4* fc.putDescendentAttributes
[docs] def putDescendentAttributes(self, p):
# Create lists of all tnodes whose vnodes are marked or expanded.
marks = []; expanded = []
for p in p.subtree():
v = p.v
if p.isMarked() and p.v not in marks:
marks.append(v)
if p.hasChildren() and p.isExpanded() and v not in expanded:
expanded.append(v)
result = []
for theList, tag in ((marks, "marks"), (expanded, "expanded")):
if theList:
sList = []
for v in theList:
sList.append("%s," % v.fileIndex)
s = ''.join(sList)
result.append('\n%s="%s"' % (tag, s))
return ''.join(result)
#@+node:ekr.20080805071954.2: *4* fc.putDescendentVnodeUas
[docs] def putDescendentVnodeUas(self, p):
'''
Return the a uA field for descendent VNode attributes,
suitable for reconstituting uA's for anonymous vnodes.
'''
#
# Create aList of tuples (p,v) having a valid unknownAttributes dict.
# Create dictionary: keys are vnodes, values are corresonding archived positions.
pDict = {}; aList = []
for p2 in p.self_and_subtree():
if hasattr(p2.v, "unknownAttributes"):
aList.append((p2.copy(), p2.v),)
pDict[p2.v] = p2.archivedPosition(root_p=p)
# Create aList of pairs (v,d) where d contains only pickleable entries.
if aList: aList = self.createUaList(aList)
if not aList: return ''
# Create d, an enclosing dict to hold all the inner dicts.
d = {}
for v, d2 in aList:
aList2 = [str(z) for z in pDict.get(v)]
key = '.'.join(aList2)
d[key] = d2
# Pickle and hexlify d
# pylint: disable=consider-using-ternary
return d and self.pickle(
torv=p.v, val=d, tag='descendentVnodeUnknownAttributes') or ''
#@+node:ekr.20050418161620.2: *4* fc.putUaHelper
[docs] def putUaHelper(self, torv, key, val):
'''Put attribute whose name is key and value is val to the output stream.'''
# New in 4.3: leave string attributes starting with 'str_' alone.
if key.startswith('str_'):
if g.isString(val) or g.isBytes(val):
val = g.toUnicode(val)
attr = ' %s="%s"' % (key, xml.sax.saxutils.escape(val))
return attr
else:
g.trace(type(val), repr(val))
g.warning("ignoring non-string attribute", key, "in", torv)
return ''
else:
return self.pickle(torv=torv, val=val, tag=key)
#@+node:EKR.20040526202501: *4* fc.putUnknownAttributes
[docs] def putUnknownAttributes(self, torv):
"""Put pickleable values for all keys in torv.unknownAttributes dictionary."""
attrDict = torv.unknownAttributes
if isinstance(attrDict, dict):
val = ''.join(
[self.putUaHelper(torv, key, val)
for key, val in attrDict.items()])
return val
else:
g.warning("ignoring non-dictionary unknownAttributes for", torv)
return ''
#@+node:ekr.20031218072017.3045: *4* fc.setDefaultDirectoryForNewFiles
[docs] def setDefaultDirectoryForNewFiles(self, fileName):
"""Set c.openDirectory for new files for the benefit of leoAtFile.scanAllDirectives."""
c = self.c
if not c.openDirectory:
theDir = g.os_path_dirname(fileName)
if theDir and g.os_path_isabs(theDir) and g.os_path_exists(theDir):
c.openDirectory = c.frame.openDirectory = theDir
#@+node:ekr.20080412172151.2: *4* fc.updateFixedStatus
[docs] def updateFixedStatus(self):
c = self.c
p = c.config.findSettingsPosition('@bool fixedWindow')
if p:
import leo.core.leoConfig as leoConfig
parser = leoConfig.SettingsTreeParser(c)
kind, name, val = parser.parseHeadline(p.h)
if val and val.lower() in ('true', '1'):
val = True
else:
val = False
c.fixed = val
#@-others
#@-others
#@@language python
#@@tabwidth -4
#@@pagewidth 70
#@-leo