Coverage for C:\leo.repo\leo-editor\leo\core\leoFileCommands.py : 49%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# -*- coding: utf-8 -*-
2#@+leo-ver=5-thin
3#@+node:ekr.20031218072017.3018: * @file leoFileCommands.py
4#@@first
5"""Classes relating to reading and writing .leo files."""
6#@+<< imports >>
7#@+node:ekr.20050405141130: ** << imports >> (leoFileCommands)
8import binascii
9from collections import defaultdict
10from contextlib import contextmanager
11import difflib
12import hashlib
13import io
14import json
15import os
16import pickle
17import shutil
18import sqlite3
19import tempfile
20import time
21from typing import Dict
22import zipfile
23import xml.etree.ElementTree as ElementTree
24import xml.sax
25import xml.sax.saxutils
26from leo.core import leoGlobals as g
27from leo.core import leoNodes
28#@-<< imports >>
29PRIVAREA = '---begin-private-area---'
30#@+others
31#@+node:ekr.20150509194827.1: ** cmd (decorator)
32def cmd(name):
33 """Command decorator for the FileCommands class."""
34 return g.new_cmd_decorator(name, ['c', 'fileCommands',])
35#@+node:ekr.20210316035506.1: ** commands (leoFileCommands.py)
36#@+node:ekr.20180708114847.1: *3* dump-clone-parents
37@g.command('dump-clone-parents')
38def dump_clone_parents(event):
39 """Print the parent vnodes of all cloned vnodes."""
40 c = event.get('c')
41 if not c:
42 return
43 print('dump-clone-parents...')
44 d = c.fileCommands.gnxDict
45 for gnx in d:
46 v = d.get(gnx)
47 if len(v.parents) > 1:
48 print(v.h)
49 g.printObj(v.parents)
50#@+node:ekr.20210309114903.1: *3* dump-gnx-dict
51@g.command('dump-gnx-dict')
52def dump_gnx_dict(event):
53 """Dump c.fileCommands.gnxDict."""
54 c = event.get('c')
55 if not c:
56 return
57 d = c.fileCommands.gnxDict
58 g.printObj(d, tag='gnxDict')
59#@+node:ekr.20060918164811: ** class BadLeoFile
60class BadLeoFile(Exception):
62 def __init__(self, message):
63 self.message = message
64 super().__init__(message)
66 def __str__(self):
67 return "Bad Leo File:" + self.message
68#@+node:ekr.20180602062323.1: ** class FastRead
69class FastRead:
71 nativeVnodeAttributes = (
72 'a',
73 'descendentTnodeUnknownAttributes',
74 'descendentVnodeUnknownAttributes',
75 'expanded', 'marks', 't',
76 # 'tnodeList', # Removed in Leo 4.7.
77 )
79 def __init__(self, c, gnx2vnode):
80 self.c = c
81 self.gnx2vnode = gnx2vnode
83 #@+others
84 #@+node:ekr.20180604110143.1: *3* fast.readFile
85 def readFile(self, theFile, path):
86 """Read the file, change splitter ratiors, and return its hidden vnode."""
87 s = theFile.read()
88 v, g_element = self.readWithElementTree(path, s)
89 if not v: # #1510.
90 return None
91 self.scanGlobals(g_element)
92 # #1047: only this method changes splitter sizes.
93 #
94 # #1111: ensure that all outlines have at least one node.
95 if not v.children:
96 new_vnode = leoNodes.VNode(context=self.c)
97 new_vnode.h = 'newHeadline'
98 v.children = [new_vnode]
99 return v
101 #@+node:ekr.20210316035646.1: *3* fast.readFileFromClipboard
102 def readFileFromClipboard(self, s):
103 """
104 Recreate a file from a string s, and return its hidden vnode.
106 Unlike readFile above, this does not affect splitter sizes.
107 """
108 v, g_element = self.readWithElementTree(path=None, s=s)
109 if not v: # #1510.
110 return None
111 #
112 # #1111: ensure that all outlines have at least one node.
113 if not v.children:
114 new_vnode = leoNodes.VNode(context=self.c)
115 new_vnode.h = 'newHeadline'
116 v.children = [new_vnode]
117 return v
118 #@+node:ekr.20180602062323.7: *3* fast.readWithElementTree & helpers
119 # #1510: https://en.wikipedia.org/wiki/Valid_characters_in_XML.
120 translate_dict = {z: None for z in range(20) if chr(z) not in '\t\r\n'}
122 def readWithElementTree(self, path, s):
124 contents = g.toUnicode(s)
125 table = contents.maketrans(self.translate_dict) # type:ignore #1510.
126 contents = contents.translate(table) # #1036, #1046.
127 try:
128 xroot = ElementTree.fromstring(contents)
129 except Exception as e:
130 # #970: Report failure here.
131 if path:
132 message = f"bad .leo file: {g.shortFileName(path)}"
133 else:
134 message = 'The clipboard is not a vaild .leo file'
135 g.es_print('\n' + message, color='red')
136 g.es_print(g.toUnicode(e))
137 print('')
138 return None, None # #1510: Return a tuple.
139 g_element = xroot.find('globals')
140 v_elements = xroot.find('vnodes')
141 t_elements = xroot.find('tnodes')
142 gnx2body, gnx2ua = self.scanTnodes(t_elements)
143 hidden_v = self.scanVnodes(gnx2body, self.gnx2vnode, gnx2ua, v_elements)
144 self.handleBits()
145 return hidden_v, g_element
146 #@+node:ekr.20180624125321.1: *4* fast.handleBits (reads c.db)
147 def handleBits(self):
148 """Restore the expanded and marked bits from c.db."""
149 c, fc = self.c, self.c.fileCommands
150 expanded = c.db.get('expanded')
151 marked = c.db.get('marked')
152 expanded = expanded.split(',') if expanded else []
153 marked = marked.split(',') if marked else []
154 fc.descendentExpandedList = expanded
155 fc.descendentMarksList = marked
156 #@+node:ekr.20180606041211.1: *4* fast.resolveUa & helper
157 def resolveUa(self, attr, val, kind=None): # Kind is for unit testing.
158 """Parse an unknown attribute in a <v> or <t> element."""
159 try:
160 val = g.toEncodedString(val)
161 except Exception:
162 g.es_print('unexpected exception converting hexlified string to string')
163 g.es_exception()
164 return None
165 # Leave string attributes starting with 'str_' alone.
166 if attr.startswith('str_'):
167 if isinstance(val, (str, bytes)):
168 return g.toUnicode(val)
169 try:
170 binString = binascii.unhexlify(val)
171 # Throws a TypeError if val is not a hex string.
172 except Exception:
173 # Assume that Leo 4.1 or above wrote the attribute.
174 if g.unitTesting:
175 assert kind == 'raw', f"unit test failed: kind={kind}"
176 else:
177 g.trace(f"can not unhexlify {attr}={val}")
178 return val
179 try:
180 # No change needed to support protocols.
181 val2 = pickle.loads(binString)
182 return val2
183 except Exception:
184 try:
185 val2 = pickle.loads(binString, encoding='bytes')
186 val2 = self.bytesToUnicode(val2)
187 return val2
188 except Exception:
189 g.trace(f"can not unpickle {attr}={val}")
190 return val
191 #@+node:ekr.20180606044154.1: *5* fast.bytesToUnicode
192 def bytesToUnicode(self, ob):
193 """
194 Recursively convert bytes objects in strings / lists / dicts to str
195 objects, thanks to TNT
196 http://stackoverflow.com/questions/22840092
197 Needed for reading Python 2.7 pickles in Python 3.4.
198 """
199 # This is simpler than using isinstance.
200 # pylint: disable=unidiomatic-typecheck
201 t = type(ob)
202 if t in (list, tuple):
203 l = [str(i, 'utf-8') if type(i) is bytes else i for i in ob]
204 l = [self.bytesToUnicode(i)
205 if type(i) in (list, tuple, dict) else i
206 for i in l]
207 ro = tuple(l) if t is tuple else l
208 elif t is dict:
209 byte_keys = [i for i in ob if type(i) is bytes]
210 for bk in byte_keys:
211 v = ob[bk]
212 del ob[bk]
213 ob[str(bk, 'utf-8')] = v
214 for k in ob:
215 if type(ob[k]) is bytes:
216 ob[k] = str(ob[k], 'utf-8')
217 elif type(ob[k]) in (list, tuple, dict):
218 ob[k] = self.bytesToUnicode(ob[k])
219 ro = ob
220 elif t is bytes: # TNB added this clause
221 ro = str(ob, 'utf-8')
222 else:
223 ro = ob
224 return ro
225 #@+node:ekr.20180605062300.1: *4* fast.scanGlobals & helper
226 def scanGlobals(self, g_element):
227 """Get global data from the cache, with reasonable defaults."""
228 c = self.c
229 d = self.getGlobalData()
230 windowSize = g.app.loadManager.options.get('windowSize')
231 windowSpot = g.app.loadManager.options.get('windowSpot')
232 if windowSize is not None:
233 h, w = windowSize # checked in LM.scanOption.
234 else:
235 w, h = d.get('width'), d.get('height')
236 if windowSpot is None:
237 x, y = d.get('left'), d.get('top')
238 else:
239 y, x = windowSpot # #1263: (top, left)
240 if 'size' in g.app.debug:
241 g.trace(w, h, x, y, c.shortFileName())
242 # c.frame may be a NullFrame.
243 c.frame.setTopGeometry(w, h, x, y)
244 r1, r2 = d.get('r1'), d.get('r2')
245 c.frame.resizePanesToRatio(r1, r2)
246 frameFactory = getattr(g.app.gui, 'frameFactory', None)
247 if not frameFactory:
248 return
249 assert frameFactory is not None
250 mf = frameFactory.masterFrame
251 if g.app.start_minimized:
252 mf.showMinimized()
253 elif g.app.start_maximized:
254 # #1189: fast.scanGlobals calls showMaximized later.
255 mf.showMaximized()
256 elif g.app.start_fullscreen:
257 mf.showFullScreen()
258 else:
259 mf.show()
260 #@+node:ekr.20180708060437.1: *5* fast.getGlobalData
261 def getGlobalData(self):
262 """Return a dict containing all global data."""
263 c = self.c
264 try:
265 window_pos = c.db.get('window_position')
266 r1 = float(c.db.get('body_outline_ratio', '0.5'))
267 r2 = float(c.db.get('body_secondary_ratio', '0.5'))
268 top, left, height, width = window_pos
269 return {
270 'top': int(top),
271 'left': int(left),
272 'height': int(height),
273 'width': int(width),
274 'r1': r1,
275 'r2': r2,
276 }
277 except Exception:
278 pass
279 # Use reasonable defaults.
280 return {
281 'top': 50, 'left': 50,
282 'height': 500, 'width': 800,
283 'r1': 0.5, 'r2': 0.5,
284 }
285 #@+node:ekr.20180602062323.8: *4* fast.scanTnodes
286 def scanTnodes(self, t_elements):
288 gnx2body: Dict[str, str] = {}
289 gnx2ua: Dict[str, dict] = defaultdict(dict)
290 for e in t_elements:
291 # First, find the gnx.
292 gnx = e.attrib['tx']
293 gnx2body[gnx] = e.text or ''
294 # Next, scan for uA's for this gnx.
295 for key, val in e.attrib.items():
296 if key != 'tx':
297 gnx2ua[gnx][key] = self.resolveUa(key, val)
298 return gnx2body, gnx2ua
299 #@+node:ekr.20180602062323.9: *4* fast.scanVnodes & helper
300 def scanVnodes(self, gnx2body, gnx2vnode, gnx2ua, v_elements):
302 c, fc = self.c, self.c.fileCommands
303 #@+<< define v_element_visitor >>
304 #@+node:ekr.20180605102822.1: *5* << define v_element_visitor >>
305 def v_element_visitor(parent_e, parent_v):
306 """Visit the given element, creating or updating the parent vnode."""
307 for e in parent_e:
308 assert e.tag in ('v', 'vh'), e.tag
309 if e.tag == 'vh':
310 parent_v._headString = g.toUnicode(e.text or '')
311 continue
312 # #1581: Attempt to handle old Leo outlines.
313 try:
314 gnx = e.attrib['t']
315 v = gnx2vnode.get(gnx)
316 except KeyError:
317 # g.trace('no "t" attrib')
318 gnx = None
319 v = None
320 if v:
321 # A clone
322 parent_v.children.append(v)
323 v.parents.append(parent_v)
324 # The body overrides any previous body text.
325 body = g.toUnicode(gnx2body.get(gnx) or '')
326 assert isinstance(body, str), body.__class__.__name__
327 v._bodyString = body
328 else:
329 #@+<< Make a new vnode, linked to the parent >>
330 #@+node:ekr.20180605075042.1: *6* << Make a new vnode, linked to the parent >>
331 v = leoNodes.VNode(context=c, gnx=gnx)
332 gnx2vnode[gnx] = v
333 parent_v.children.append(v)
334 v.parents.append(parent_v)
335 body = g.toUnicode(gnx2body.get(gnx) or '')
336 assert isinstance(body, str), body.__class__.__name__
337 v._bodyString = body
338 v._headString = 'PLACE HOLDER'
339 #@-<< Make a new vnode, linked to the parent >>
340 #@+<< handle all other v attributes >>
341 #@+node:ekr.20180605075113.1: *6* << handle all other v attributes >>
342 # FastRead.nativeVnodeAttributes defines the native attributes of <v> elements.
343 d = e.attrib
344 s = d.get('descendentTnodeUnknownAttributes')
345 if s:
346 aDict = fc.getDescendentUnknownAttributes(s, v=v)
347 if aDict:
348 fc.descendentTnodeUaDictList.append(aDict)
349 s = d.get('descendentVnodeUnknownAttributes')
350 if s:
351 aDict = fc.getDescendentUnknownAttributes(s, v=v)
352 if aDict:
353 fc.descendentVnodeUaDictList.append((v, aDict),)
354 #
355 # Handle vnode uA's
356 uaDict = gnx2ua[gnx] # A defaultdict(dict)
357 for key, val in d.items():
358 if key not in self.nativeVnodeAttributes:
359 uaDict[key] = self.resolveUa(key, val)
360 if uaDict:
361 v.unknownAttributes = uaDict
362 #@-<< handle all other v attributes >>
363 # Handle all inner elements.
364 v_element_visitor(e, v)
366 #@-<< define v_element_visitor >>
367 #
368 # Create the hidden root vnode.
370 gnx = 'hidden-root-vnode-gnx'
371 hidden_v = leoNodes.VNode(context=c, gnx=gnx)
372 hidden_v._headString = '<hidden root vnode>'
373 gnx2vnode[gnx] = hidden_v
374 #
375 # Traverse the tree of v elements.
376 v_element_visitor(v_elements, hidden_v)
377 return hidden_v
378 #@-others
379#@+node:ekr.20160514120347.1: ** class FileCommands
380class FileCommands:
381 """A class creating the FileCommands subcommander."""
382 #@+others
383 #@+node:ekr.20090218115025.4: *3* fc.Birth
384 #@+node:ekr.20031218072017.3019: *4* fc.ctor
385 def __init__(self, c):
386 """Ctor for FileCommands class."""
387 self.c = c
388 self.frame = c.frame
389 self.nativeTnodeAttributes = ('tx',)
390 self.nativeVnodeAttributes = (
391 'a',
392 'descendentTnodeUnknownAttributes',
393 'descendentVnodeUnknownAttributes', # New in Leo 4.5.
394 'expanded', 'marks', 't',
395 # 'tnodeList', # Removed in Leo 4.7.
396 )
397 self.initIvars()
398 #@+node:ekr.20090218115025.5: *4* fc.initIvars
399 def initIvars(self):
400 """Init ivars of the FileCommands class."""
401 # General...
402 c = self.c
403 self.mFileName = ""
404 self.fileDate = -1
405 self.leo_file_encoding = c.config.new_leo_file_encoding
406 # For reading...
407 self.checking = False # True: checking only: do *not* alter the outline.
408 self.descendentExpandedList = []
409 self.descendentMarksList = []
410 self.forbiddenTnodes = []
411 self.descendentTnodeUaDictList = []
412 self.descendentVnodeUaDictList = []
413 self.ratio = 0.5
414 self.currentVnode = None
415 # For writing...
416 self.read_only = False
417 self.rootPosition = None
418 self.outputFile = None
419 self.openDirectory = None
420 self.usingClipboard = False
421 self.currentPosition = None
422 # New in 3.12...
423 self.copiedTree = None
424 # fc.gnxDict is never re-inited.
425 self.gnxDict = {} # Keys are gnx strings. Values are vnodes.
426 self.vnodesDict = {} # keys are gnx strings; values are ignored
427 #@+node:ekr.20210316042224.1: *3* fc: Commands
428 #@+node:ekr.20031218072017.2012: *4* fc.writeAtFileNodes
429 @cmd('write-at-file-nodes')
430 def writeAtFileNodes(self, event=None):
431 """Write all @file nodes in the selected outline."""
432 c = self.c
433 c.endEditing()
434 c.init_error_dialogs()
435 c.atFileCommands.writeAll(all=True)
436 c.raise_error_dialogs(kind='write')
437 #@+node:ekr.20031218072017.3050: *4* fc.write-outline-only
438 @cmd('write-outline-only')
439 def writeOutlineOnly(self, event=None):
440 """Write the entire outline without writing any derived files."""
441 c = self.c
442 c.endEditing()
443 self.writeOutline(fileName=self.mFileName)
445 #@+node:ekr.20031218072017.1666: *4* fc.writeDirtyAtFileNodes
446 @cmd('write-dirty-at-file-nodes')
447 def writeDirtyAtFileNodes(self, event=None):
448 """Write all changed @file Nodes."""
449 c = self.c
450 c.endEditing()
451 c.init_error_dialogs()
452 c.atFileCommands.writeAll(dirty=True)
453 c.raise_error_dialogs(kind='write')
454 #@+node:ekr.20031218072017.2013: *4* fc.writeMissingAtFileNodes
455 @cmd('write-missing-at-file-nodes')
456 def writeMissingAtFileNodes(self, event=None):
457 """Write all @file nodes for which the corresponding external file does not exist."""
458 c = self.c
459 c.endEditing()
460 c.atFileCommands.writeMissing(c.p)
461 #@+node:ekr.20210316034350.1: *3* fc: File Utils
462 #@+node:ekr.20031218072017.3047: *4* fc.createBackupFile
463 def createBackupFile(self, fileName):
464 """
465 Create a closed backup file and copy the file to it,
466 but only if the original file exists.
467 """
468 if g.os_path_exists(fileName):
469 fd, backupName = tempfile.mkstemp(text=False)
470 f = open(fileName, 'rb') # rb is essential.
471 s = f.read()
472 f.close()
473 try:
474 try:
475 os.write(fd, s)
476 finally:
477 os.close(fd)
478 ok = True
479 except Exception:
480 g.error('exception creating backup file')
481 g.es_exception()
482 ok, backupName = False, None
483 if not ok and self.read_only:
484 g.error("read only")
485 else:
486 ok, backupName = True, None
487 return ok, backupName
488 #@+node:ekr.20050404190914.2: *4* fc.deleteBackupFile
489 def deleteBackupFile(self, fileName):
490 try:
491 os.remove(fileName)
492 except Exception:
493 if self.read_only:
494 g.error("read only")
495 g.error("exception deleting backup file:", fileName)
496 g.es_exception(full=False)
497 #@+node:ekr.20100119145629.6108: *4* fc.handleWriteLeoFileException
498 def handleWriteLeoFileException(self, fileName, backupName, f):
499 """Report an exception. f is an open file, or None."""
500 # c = self.c
501 g.es("exception writing:", fileName)
502 g.es_exception(full=True)
503 if f:
504 f.close()
505 # Delete fileName.
506 if fileName and g.os_path_exists(fileName):
507 self.deleteBackupFile(fileName)
508 # Rename backupName to fileName.
509 if backupName and g.os_path_exists(backupName):
510 g.es("restoring", fileName, "from", backupName)
511 # No need to create directories when restoring.
512 src, dst = backupName, fileName
513 try:
514 shutil.move(src, dst)
515 except Exception:
516 g.error('exception renaming', src, 'to', dst)
517 g.es_exception(full=False)
518 else:
519 g.error('backup file does not exist!', repr(backupName))
520 #@+node:ekr.20040324080359.1: *4* fc.isReadOnly
521 def isReadOnly(self, fileName):
522 # self.read_only is not valid for Save As and Save To commands.
523 if g.os_path_exists(fileName):
524 try:
525 if not os.access(fileName, os.W_OK):
526 g.error("can not write: read only:", fileName)
527 return True
528 except Exception:
529 pass # os.access() may not exist on all platforms.
530 return False
531 #@+node:ekr.20210315031535.1: *4* fc.openOutlineForWriting
532 def openOutlineForWriting(self, fileName):
533 """Open a .leo file for writing. Return the open file, or None."""
534 try:
535 f = open(fileName, 'wb') # Always use binary mode.
536 except Exception:
537 g.es(f"can not open {fileName}")
538 g.es_exception()
539 f = None
540 return f
541 #@+node:ekr.20031218072017.3045: *4* fc.setDefaultDirectoryForNewFiles
542 def setDefaultDirectoryForNewFiles(self, fileName):
543 """Set c.openDirectory for new files for the benefit of leoAtFile.scanAllDirectives."""
544 c = self.c
545 if not c.openDirectory:
546 theDir = g.os_path_dirname(fileName)
547 if theDir and g.os_path_isabs(theDir) and g.os_path_exists(theDir):
548 c.openDirectory = c.frame.openDirectory = theDir
549 #@+node:ekr.20031218072017.1554: *4* fc.warnOnReadOnlyFiles
550 def warnOnReadOnlyFiles(self, fileName):
551 # os.access may not exist on all platforms.
552 try:
553 self.read_only = not os.access(fileName, os.W_OK)
554 except AttributeError:
555 self.read_only = False
556 except UnicodeError:
557 self.read_only = False
558 if self.read_only and not g.unitTesting:
559 g.error("read only:", fileName)
560 #@+node:ekr.20031218072017.3020: *3* fc: Reading
561 #@+node:ekr.20031218072017.1559: *4* fc: Paste
562 #@+node:ekr.20080410115129.1: *5* fc.checkPaste
563 def checkPaste(self, parent, p):
564 """Return True if p may be pasted as a child of parent."""
565 if not parent:
566 return True
567 parents = list(parent.self_and_parents())
568 for p in p.self_and_subtree(copy=False):
569 for z in parents:
570 if p.v == z.v:
571 g.warning('Invalid paste: nodes may not descend from themselves')
572 return False
573 return True
574 #@+node:ekr.20180709205603.1: *5* fc.getLeoOutlineFromClipBoard
575 def getLeoOutlineFromClipboard(self, s):
576 """Read a Leo outline from string s in clipboard format."""
577 c = self.c
578 current = c.p
579 if not current:
580 g.trace('no c.p')
581 return None
582 self.initReadIvars()
583 # Save the hidden root's children.
584 old_children = c.hiddenRootNode.children
585 # Save and clear gnxDict.
586 oldGnxDict = self.gnxDict
587 self.gnxDict = {}
588 s = g.toEncodedString(s, self.leo_file_encoding, reportErrors=True)
589 # This encoding must match the encoding used in outline_to_clipboard_string.
590 hidden_v = FastRead(c, self.gnxDict).readFileFromClipboard(s)
591 v = hidden_v.children[0]
592 v.parents = []
593 # Restore the hidden root's children
594 c.hiddenRootNode.children = old_children
595 if not v:
596 return g.es("the clipboard is not valid ", color="blue")
597 # Create the position.
598 p = leoNodes.Position(v)
599 # Do *not* adjust links when linking v.
600 if current.hasChildren() and current.isExpanded():
601 p._linkCopiedAsNthChild(current, 0)
602 else:
603 p._linkCopiedAfter(current)
604 assert not p.isCloned(), g.objToString(p.v.parents)
605 self.gnxDict = oldGnxDict
606 self.reassignAllIndices(p)
607 c.selectPosition(p)
608 self.initReadIvars()
609 return p
611 getLeoOutline = getLeoOutlineFromClipboard # for compatibility
612 #@+node:ekr.20180709205640.1: *5* fc.getLeoOutlineFromClipBoardRetainingClones
613 def getLeoOutlineFromClipboardRetainingClones(self, s):
614 """Read a Leo outline from string s in clipboard format."""
615 c = self.c
616 current = c.p
617 if not current:
618 return g.trace('no c.p')
619 self.initReadIvars()
620 # Save the hidden root's children.
621 old_children = c.hiddenRootNode.children
622 # All pasted nodes should already have unique gnx's.
623 ni = g.app.nodeIndices
624 for v in c.all_unique_nodes():
625 ni.check_gnx(c, v.fileIndex, v)
626 s = g.toEncodedString(s, self.leo_file_encoding, reportErrors=True)
627 # This encoding must match the encoding used in outline_to_clipboard_string.
628 hidden_v = FastRead(c, self.gnxDict).readFileFromClipboard(s)
629 v = hidden_v.children[0]
630 v.parents.remove(hidden_v)
631 # Restore the hidden root's children
632 c.hiddenRootNode.children = old_children
633 if not v:
634 return g.es("the clipboard is not valid ", color="blue")
635 # Create the position.
636 p = leoNodes.Position(v)
637 # Do *not* adjust links when linking v.
638 if current.hasChildren() and current.isExpanded():
639 if not self.checkPaste(current, p):
640 return None
641 p._linkCopiedAsNthChild(current, 0)
642 else:
643 if not self.checkPaste(current.parent(), p):
644 return None
645 p._linkCopiedAfter(current)
646 # Fix #862: paste-retaining-clones can corrupt the outline.
647 self.linkChildrenToParents(p)
648 c.selectPosition(p)
649 self.initReadIvars()
650 return p
651 #@+node:ekr.20180424123010.1: *5* fc.linkChildrenToParents
652 def linkChildrenToParents(self, p):
653 """
654 Populate the parent links in all children of p.
655 """
656 for child in p.children():
657 if not child.v.parents:
658 child.v.parents.append(p.v)
659 self.linkChildrenToParents(child)
660 #@+node:ekr.20180425034856.1: *5* fc.reassignAllIndices
661 def reassignAllIndices(self, p):
662 """Reassign all indices in p's subtree."""
663 ni = g.app.nodeIndices
664 for p2 in p.self_and_subtree(copy=False):
665 v = p2.v
666 index = ni.getNewIndex(v)
667 if 'gnx' in g.app.debug:
668 g.trace('**reassigning**', index, v)
669 #@+node:ekr.20060919104836: *4* fc: Read Top-level
670 #@+node:ekr.20031218072017.1553: *5* fc.getLeoFile (read switch)
671 def getLeoFile(self,
672 theFile,
673 fileName,
674 readAtFileNodesFlag=True,
675 silent=False,
676 checkOpenFiles=True,
677 ):
678 """
679 Read a .leo file.
680 The caller should follow this with a call to c.redraw().
681 """
682 fc, c = self, self.c
683 t1 = time.time()
684 c.clearChanged() # May be set when reading @file nodes.
685 fc.warnOnReadOnlyFiles(fileName)
686 fc.checking = False
687 fc.mFileName = c.mFileName
688 fc.initReadIvars()
689 recoveryNode = None
690 try:
691 c.loading = True # disable c.changed
692 if not silent and checkOpenFiles:
693 # Don't check for open file when reverting.
694 g.app.checkForOpenFile(c, fileName)
695 #
696 # Read the .leo file and create the outline.
697 if fileName.endswith('.db'):
698 v = fc.retrieveVnodesFromDb(theFile) or fc.initNewDb(theFile)
699 elif fileName.endswith('.leojs'):
700 v = fc.read_leojs(theFile, fileName)
701 readAtFileNodesFlag = False # Suppress post-processing.
702 else:
703 v = FastRead(c, self.gnxDict).readFile(theFile, fileName)
704 if v:
705 c.hiddenRootNode = v
706 if v:
707 c.setFileTimeStamp(fileName)
708 if readAtFileNodesFlag:
709 recoveryNode = fc.readExternalFiles(fileName)
710 finally:
711 p = recoveryNode or c.p or c.lastTopLevel()
712 # lastTopLevel is a better fallback, imo.
713 c.selectPosition(p)
714 c.redraw_later()
715 # Delay the second redraw until idle time.
716 # This causes a slight flash, but corrects a hangnail.
717 c.checkOutline()
718 # Must be called *after* ni.end_holding.
719 c.loading = False
720 # reenable c.changed
721 if not isinstance(theFile, sqlite3.Connection):
722 theFile.close()
723 # Fix bug https://bugs.launchpad.net/leo-editor/+bug/1208942
724 # Leo holding directory/file handles after file close?
725 if c.changed:
726 fc.propagateDirtyNodes()
727 fc.initReadIvars()
728 t2 = time.time()
729 g.es(f"read outline in {t2 - t1:2.2f} seconds")
730 return v, c.frame.ratio
731 #@+node:ekr.20031218072017.2297: *5* fc.openLeoFile
732 def openLeoFile(self, theFile, fileName, readAtFileNodesFlag=True, silent=False):
733 """
734 Open a Leo file.
736 readAtFileNodesFlag: False when reading settings files.
737 silent: True when creating hidden commanders.
738 """
739 c, frame = self.c, self.c.frame
740 # Set c.openDirectory
741 theDir = g.os_path_dirname(fileName)
742 if theDir:
743 c.openDirectory = c.frame.openDirectory = theDir
744 # Get the file.
745 self.gnxDict = {} # #1437
746 ok, ratio = self.getLeoFile(
747 theFile, fileName,
748 readAtFileNodesFlag=readAtFileNodesFlag,
749 silent=silent,
750 )
751 if ok:
752 frame.resizePanesToRatio(ratio, frame.secondary_ratio)
753 return ok
754 #@+node:ekr.20120212220616.10537: *5* fc.readExternalFiles & helper
755 def readExternalFiles(self, fileName):
756 """
757 Read all external files.
759 A helper for fc.getLeoFile.
760 """
761 c, fc = self.c, self
762 c.atFileCommands.readAll(c.rootPosition())
763 recoveryNode = fc.handleNodeConflicts()
764 #
765 # Do this after reading external files.
766 # The descendent nodes won't exist unless we have read
767 # the @thin nodes!
768 fc.restoreDescendentAttributes()
769 fc.setPositionsFromVnodes()
770 return recoveryNode
771 #@+node:ekr.20100205060712.8314: *6* fc.handleNodeConflicts
772 def handleNodeConflicts(self):
773 """Create a 'Recovered Nodes' node for each entry in c.nodeConflictList."""
774 c = self.c
775 if not c.nodeConflictList:
776 return None
777 if not c.make_node_conflicts_node:
778 s = f"suppressed {len(c.nodeConflictList)} node conflicts"
779 g.es(s, color='red')
780 g.pr('\n' + s + '\n')
781 return None
782 # Create the 'Recovered Nodes' node.
783 last = c.lastTopLevel()
784 root = last.insertAfter()
785 root.setHeadString('Recovered Nodes')
786 root.expand()
787 # For each conflict, create one child and two grandchildren.
788 for bunch in c.nodeConflictList:
789 tag = bunch.get('tag') or ''
790 gnx = bunch.get('gnx') or ''
791 fn = bunch.get('fileName') or ''
792 b1, h1 = bunch.get('b_old'), bunch.get('h_old')
793 b2, h2 = bunch.get('b_new'), bunch.get('h_new')
794 root_v = bunch.get('root_v') or ''
795 child = root.insertAsLastChild()
796 h = f'Recovered node "{h1}" from {g.shortFileName(fn)}'
797 child.setHeadString(h)
798 if b1 == b2:
799 lines = [
800 'Headline changed...',
801 f"{tag} gnx: {gnx} root: {(root_v and root.v)!r}",
802 f"old headline: {h1}",
803 f"new headline: {h2}",
804 ]
805 child.setBodyString('\n'.join(lines))
806 else:
807 line1 = f"{tag} gnx: {gnx} root: {root_v and root.v!r}\nDiff...\n"
808 d = difflib.Differ().compare(g.splitLines(b1), g.splitLines(b2))
809 # 2017/06/19: reverse comparison order.
810 diffLines = [z for z in d]
811 lines = [line1]
812 lines.extend(diffLines)
813 # There is less need to show trailing newlines because
814 # we don't report changes involving only trailing newlines.
815 child.setBodyString(''.join(lines))
816 n1 = child.insertAsNthChild(0)
817 n2 = child.insertAsNthChild(1)
818 n1.setHeadString('old:' + h1)
819 n1.setBodyString(b1)
820 n2.setHeadString('new:' + h2)
821 n2.setBodyString(b2)
822 return root
823 #@+node:ekr.20031218072017.3030: *5* fc.readOutlineOnly
824 def readOutlineOnly(self, theFile, fileName):
825 c = self.c
826 # Set c.openDirectory
827 theDir = g.os_path_dirname(fileName)
828 if theDir:
829 c.openDirectory = c.frame.openDirectory = theDir
830 ok, ratio = self.getLeoFile(theFile, fileName, readAtFileNodesFlag=False)
831 c.redraw()
832 c.frame.deiconify()
833 junk, junk, secondary_ratio = self.frame.initialRatios()
834 c.frame.resizePanesToRatio(ratio, secondary_ratio)
835 return ok
836 #@+node:vitalije.20170630152841.1: *5* fc.retrieveVnodesFromDb & helpers
837 def retrieveVnodesFromDb(self, conn):
838 """
839 Recreates tree from the data contained in table vnodes.
841 This method follows behavior of readSaxFile.
842 """
844 c, fc = self.c, self
845 sql = '''select gnx, head,
846 body,
847 children,
848 parents,
849 iconVal,
850 statusBits,
851 ua from vnodes'''
852 vnodes = []
853 try:
854 for row in conn.execute(sql):
855 (gnx, h, b, children, parents, iconVal, statusBits, ua) = row
856 try:
857 ua = pickle.loads(g.toEncodedString(ua))
858 except ValueError:
859 ua = None
860 v = leoNodes.VNode(context=c, gnx=gnx)
861 v._headString = h
862 v._bodyString = b
863 v.children = children.split()
864 v.parents = parents.split()
865 v.iconVal = iconVal
866 v.statusBits = statusBits
867 v.u = ua
868 vnodes.append(v)
869 except sqlite3.Error as er:
870 if er.args[0].find('no such table') < 0:
871 # there was an error raised but it is not the one we expect
872 g.internalError(er)
873 # there is no vnodes table
874 return None
876 rootChildren = [x for x in vnodes if 'hidden-root-vnode-gnx' in x.parents]
877 if not rootChildren:
878 g.trace('there should be at least one top level node!')
879 return None
881 findNode = lambda x: fc.gnxDict.get(x, c.hiddenRootNode)
883 # let us replace every gnx with the corresponding vnode
884 for v in vnodes:
885 v.children = [findNode(x) for x in v.children]
886 v.parents = [findNode(x) for x in v.parents]
887 c.hiddenRootNode.children = rootChildren
888 (w, h, x, y, r1, r2, encp) = fc.getWindowGeometryFromDb(conn)
889 c.frame.setTopGeometry(w, h, x, y)
890 c.frame.resizePanesToRatio(r1, r2)
891 p = fc.decodePosition(encp)
892 c.setCurrentPosition(p)
893 return rootChildren[0]
894 #@+node:vitalije.20170815162307.1: *6* fc.initNewDb
895 def initNewDb(self, conn):
896 """ Initializes tables and returns None"""
897 c, fc = self.c, self
898 v = leoNodes.VNode(context=c)
899 c.hiddenRootNode.children = [v]
900 (w, h, x, y, r1, r2, encp) = fc.getWindowGeometryFromDb(conn)
901 c.frame.setTopGeometry(w, h, x, y)
902 c.frame.resizePanesToRatio(r1, r2)
903 c.sqlite_connection = conn
904 fc.exportToSqlite(c.mFileName)
905 return v
906 #@+node:vitalije.20170630200802.1: *6* fc.getWindowGeometryFromDb
907 def getWindowGeometryFromDb(self, conn):
908 geom = (600, 400, 50, 50, 0.5, 0.5, '')
909 keys = ('width', 'height', 'left', 'top',
910 'ratio', 'secondary_ratio',
911 'current_position')
912 try:
913 d = dict(
914 conn.execute(
915 '''select * from extra_infos
916 where name in (?, ?, ?, ?, ?, ?, ?)''',
917 keys,
918 ).fetchall(),
919 )
920 # mypy complained that geom must be a tuple, not a generator.
921 geom = tuple(d.get(*x) for x in zip(keys, geom)) # type:ignore
922 except sqlite3.OperationalError:
923 pass
924 return geom
925 #@+node:vitalije.20170831154734.1: *5* fc.setReferenceFile
926 def setReferenceFile(self, fileName):
927 c = self.c
928 for v in c.hiddenRootNode.children:
929 if v.h == PRIVAREA:
930 v.b = fileName
931 break
932 else:
933 v = c.rootPosition().insertBefore().v
934 v.h = PRIVAREA
935 v.b = fileName
936 c.redraw()
937 g.es('set reference file:', g.shortFileName(fileName))
938 #@+node:vitalije.20170831144643.1: *5* fc.updateFromRefFile
939 def updateFromRefFile(self):
940 """Updates public part of outline from the specified file."""
941 c, fc = self.c, self
942 #@+others
943 #@+node:vitalije.20170831144827.2: *6* function: get_ref_filename
944 def get_ref_filename():
945 for v in priv_vnodes():
946 return g.splitLines(v.b)[0].strip()
947 #@+node:vitalije.20170831144827.4: *6* function: pub_vnodes
948 def pub_vnodes():
949 for v in c.hiddenRootNode.children:
950 if v.h == PRIVAREA:
951 break
952 yield v
953 #@+node:vitalije.20170831144827.5: *6* function: priv_vnodes
954 def priv_vnodes():
955 pub = True
956 for v in c.hiddenRootNode.children:
957 if v.h == PRIVAREA:
958 pub = False
959 if pub:
960 continue
961 yield v
962 #@+node:vitalije.20170831144827.6: *6* function: pub_gnxes
963 def sub_gnxes(children):
964 for v in children:
965 yield v.gnx
966 for gnx in sub_gnxes(v.children):
967 yield gnx
969 def pub_gnxes():
970 return sub_gnxes(pub_vnodes())
972 def priv_gnxes():
973 return sub_gnxes(priv_vnodes())
974 #@+node:vitalije.20170831144827.7: *6* function: restore_priv
975 def restore_priv(prdata, topgnxes):
976 vnodes = []
977 for row in prdata:
978 (gnx, h, b, children, parents, iconVal, statusBits, ua) = row
979 v = leoNodes.VNode(context=c, gnx=gnx)
980 v._headString = h
981 v._bodyString = b
982 v.children = children
983 v.parents = parents
984 v.iconVal = iconVal
985 v.statusBits = statusBits
986 v.u = ua
987 vnodes.append(v)
988 pv = lambda x: fc.gnxDict.get(x, c.hiddenRootNode)
989 for v in vnodes:
990 v.children = [pv(x) for x in v.children]
991 v.parents = [pv(x) for x in v.parents]
992 for gnx in topgnxes:
993 v = fc.gnxDict[gnx]
994 c.hiddenRootNode.children.append(v)
995 if gnx in pubgnxes:
996 v.parents.append(c.hiddenRootNode)
997 #@+node:vitalije.20170831144827.8: *6* function: priv_data
998 def priv_data(gnxes):
999 dbrow = lambda v: (
1000 v.gnx,
1001 v.h,
1002 v.b,
1003 [x.gnx for x in v.children],
1004 [x.gnx for x in v.parents],
1005 v.iconVal,
1006 v.statusBits,
1007 v.u
1008 )
1009 return tuple(dbrow(fc.gnxDict[x]) for x in gnxes)
1010 #@+node:vitalije.20170831144827.9: *6* function: nosqlite_commander
1011 @contextmanager
1012 def nosqlite_commander(fname):
1013 oldname = c.mFileName
1014 conn = getattr(c, 'sqlite_connection', None)
1015 c.sqlite_connection = None
1016 c.mFileName = fname
1017 yield c
1018 if c.sqlite_connection:
1019 c.sqlite_connection.close()
1020 c.mFileName = oldname
1021 c.sqlite_connection = conn
1022 #@-others
1023 pubgnxes = set(pub_gnxes())
1024 privgnxes = set(priv_gnxes())
1025 privnodes = priv_data(privgnxes - pubgnxes)
1026 toppriv = [v.gnx for v in priv_vnodes()]
1027 fname = get_ref_filename()
1028 with nosqlite_commander(fname):
1029 theFile = open(fname, 'rb')
1030 fc.initIvars()
1031 fc.getLeoFile(theFile, fname, checkOpenFiles=False)
1032 restore_priv(privnodes, toppriv)
1033 c.redraw()
1034 #@+node:ekr.20210316043902.1: *5* fc.read_leojs & helpers
1035 def read_leojs(self, theFile, fileName):
1036 """Read a JSON (.leojs) file and create the outline."""
1037 c = self.c
1038 s = theFile.read()
1039 try:
1040 d = json.loads(s)
1041 except Exception:
1042 g.trace(f"Error reading .leojs file: {fileName}")
1043 g.es_exception()
1044 return None
1045 #
1046 # Get the top-level dicts.
1047 tnodes_dict = d.get('tnodes')
1048 vnodes_list = d.get('vnodes')
1049 if not tnodes_dict:
1050 g.trace(f"Bad .leojs file: no tnodes dict: {fileName}")
1051 return None
1052 if not vnodes_list:
1053 g.trace(f"Bad .leojs file: no vnodes list: {fileName}")
1054 return None
1055 #
1056 # Define function: create_vnode_from_dicts.
1057 #@+others
1058 #@+node:ekr.20210317155137.1: *6* function: create_vnode_from_dicts
1059 def create_vnode_from_dicts(i, parent_v, v_dict):
1060 """Create a new vnode as the i'th child of the parent vnode."""
1061 #
1062 # Get the gnx.
1063 gnx = v_dict.get('gnx')
1064 if not gnx:
1065 g.trace(f"Bad .leojs file: no gnx in v_dict: {fileName}")
1066 g.printObj(v_dict)
1067 return
1068 #
1069 # Create the vnode.
1070 assert len(parent_v.children) == i, (i, parent_v, parent_v.children)
1071 v = leoNodes.VNode(context=c, gnx=gnx)
1072 parent_v.children.append(v)
1073 v._headString = v_dict.get('vh', '')
1074 v._bodyString = tnodes_dict.get(gnx, '')
1075 #
1076 # Recursively create the children.
1077 for i2, v_dict2 in enumerate(v_dict.get('children', [])):
1078 create_vnode_from_dicts(i2, v, v_dict2)
1079 #@+node:ekr.20210318125522.1: *6* function: scan_leojs_globals
1080 def scan_leojs_globals(json_d):
1081 """Set the geometries from the globals dict."""
1083 def toInt(x, default):
1084 try:
1085 return int(x)
1086 except Exception:
1087 return default
1089 # Priority 1: command-line args
1090 windowSize = g.app.loadManager.options.get('windowSize')
1091 windowSpot = g.app.loadManager.options.get('windowSpot')
1092 #
1093 # Priority 2: The cache.
1094 db_top, db_left, db_height, db_width = c.db.get('window_position', (None, None, None, None))
1095 #
1096 # Priority 3: The globals dict in the .leojs file.
1097 # Leo doesn't write the globals element, but leoInteg might.
1098 d = json_d.get('globals', {})
1099 #
1100 # height & width
1101 height, width = windowSize or (None, None)
1102 if height is None:
1103 height, width = d.get('height'), d.get('width')
1104 if height is None:
1105 height, width = db_height, db_width
1106 height, width = toInt(height, 500), toInt(width, 800)
1107 #
1108 # top, left.
1109 top, left = windowSpot or (None, None)
1110 if top is None:
1111 top, left = d.get('top'), d.get('left')
1112 if top is None:
1113 top, left = db_top, db_left
1114 top, left = toInt(top, 50), toInt(left, 50)
1115 #
1116 # r1, r2.
1117 r1 = float(c.db.get('body_outline_ratio', '0.5'))
1118 r2 = float(c.db.get('body_secondary_ratio', '0.5'))
1119 if 'size' in g.app.debug:
1120 g.trace(width, height, left, top, c.shortFileName())
1121 # c.frame may be a NullFrame.
1122 c.frame.setTopGeometry(width, height, left, top)
1123 c.frame.resizePanesToRatio(r1, r2)
1124 frameFactory = getattr(g.app.gui, 'frameFactory', None)
1125 if not frameFactory:
1126 return
1127 assert frameFactory is not None
1128 mf = frameFactory.masterFrame
1129 if g.app.start_minimized:
1130 mf.showMinimized()
1131 elif g.app.start_maximized:
1132 # #1189: fast.scanGlobals calls showMaximized later.
1133 mf.showMaximized()
1134 elif g.app.start_fullscreen:
1135 mf.showFullScreen()
1136 else:
1137 mf.show()
1138 #@-others
1139 #
1140 # Start the recursion by creating the top-level vnodes.
1141 c.hiddenRootNode.children = [] # Necessary.
1142 parent_v = c.hiddenRootNode
1143 for i, v_dict in enumerate(vnodes_list):
1144 create_vnode_from_dicts(i, parent_v, v_dict)
1145 scan_leojs_globals(d)
1146 return c.hiddenRootNode.children[0]
1147 #@+node:ekr.20060919133249: *4* fc: Read Utils
1148 # Methods common to both the sax and non-sax code.
1149 #@+node:ekr.20061006104837.1: *5* fc.archivedPositionToPosition
1150 def archivedPositionToPosition(self, s):
1151 """Convert an archived position (a string) to a position."""
1152 return self.c.archivedPositionToPosition(s)
1153 #@+node:ekr.20040701065235.1: *5* fc.getDescendentAttributes
1154 def getDescendentAttributes(self, s, tag=""):
1155 """s is a list of gnx's, separated by commas from a <v> or <t> element.
1156 Parses s into a list.
1158 This is used to record marked and expanded nodes.
1159 """
1160 gnxs = s.split(',')
1161 result = [gnx for gnx in gnxs if len(gnx) > 0]
1162 return result
1163 #@+node:EKR.20040627114602: *5* fc.getDescendentUnknownAttributes
1164 # Pre Leo 4.5 Only @thin vnodes had the descendentTnodeUnknownAttributes field.
1165 # New in Leo 4.5: @thin & @shadow vnodes have descendentVnodeUnknownAttributes field.
1167 def getDescendentUnknownAttributes(self, s, v=None):
1168 """Unhexlify and unpickle t/v.descendentUnknownAttribute field."""
1169 try:
1170 # Changed in version 3.2: Accept only bytestring or bytearray objects as input.
1171 s = g.toEncodedString(s) # 2011/02/22
1172 bin = binascii.unhexlify(s)
1173 # Throws a TypeError if val is not a hex string.
1174 val = pickle.loads(bin)
1175 return val
1176 except Exception:
1177 g.es_exception()
1178 g.trace('Can not unpickle', type(s), v and v.h, s[:40])
1179 return None
1180 #@+node:vitalije.20180304190953.1: *5* fc.getPos/VnodeFromClipboard
1181 def getPosFromClipboard(self, s):
1182 """A utility called from init_tree_abbrev."""
1183 v = self.getVnodeFromClipboard(s)
1184 return leoNodes.Position(v)
1186 def getVnodeFromClipboard(self, s):
1187 """Called only from getPosFromClipboard."""
1188 c = self.c
1189 self.initReadIvars()
1190 oldGnxDict = self.gnxDict
1191 self.gnxDict = {} # Fix #943
1192 try:
1193 # This encoding must match the encoding used in outline_to_clipboard_string.
1194 s = g.toEncodedString(s, self.leo_file_encoding, reportErrors=True)
1195 v = FastRead(c, {}).readFileFromClipboard(s)
1196 if not v:
1197 return g.es("the clipboard is not valid ", color="blue")
1198 finally:
1199 self.gnxDict = oldGnxDict
1200 return v
1201 #@+node:ekr.20060919142200.1: *5* fc.initReadIvars
1202 def initReadIvars(self):
1203 self.descendentTnodeUaDictList = []
1204 self.descendentVnodeUaDictList = []
1205 self.descendentExpandedList = []
1206 self.descendentMarksList = []
1207 # 2011/12/10: never re-init this dict.
1208 # self.gnxDict = {}
1209 self.c.nodeConflictList = [] # 2010/01/05
1210 self.c.nodeConflictFileName = None # 2010/01/05
1211 #@+node:ekr.20100124110832.6212: *5* fc.propagateDirtyNodes
1212 def propagateDirtyNodes(self):
1213 c = self.c
1214 aList = [z for z in c.all_positions() if z.isDirty()]
1215 for p in aList:
1216 p.setAllAncestorAtFileNodesDirty()
1217 #@+node:ekr.20080805132422.3: *5* fc.resolveArchivedPosition
1218 def resolveArchivedPosition(self, archivedPosition, root_v):
1219 """
1220 Return a VNode corresponding to the archived position relative to root
1221 node root_v.
1222 """
1224 def oops(message):
1225 """Give an error only if no file errors have been seen."""
1226 return None
1228 try:
1229 aList = [int(z) for z in archivedPosition.split('.')]
1230 aList.reverse()
1231 except Exception:
1232 return oops(f'"{archivedPosition}"')
1233 if not aList:
1234 return oops('empty')
1235 last_v = root_v
1236 n = aList.pop()
1237 if n != 0:
1238 return oops(f'root index="{n}"')
1239 while aList:
1240 n = aList.pop()
1241 children = last_v.children
1242 if n < len(children):
1243 last_v = children[n]
1244 else:
1245 return oops(f'bad index="{n}", len(children)="{len(children)}"')
1246 return last_v
1247 #@+node:EKR.20040627120120: *5* fc.restoreDescendentAttributes
1248 def restoreDescendentAttributes(self):
1249 """Called from fc.readExternalFiles."""
1250 c = self.c
1251 for resultDict in self.descendentTnodeUaDictList:
1252 for gnx in resultDict:
1253 v = self.gnxDict.get(gnx)
1254 if v:
1255 v.unknownAttributes = resultDict[gnx]
1256 v._p_changed = True
1257 # New in Leo 4.5: keys are archivedPositions, values are attributes.
1258 for root_v, resultDict in self.descendentVnodeUaDictList:
1259 for key in resultDict:
1260 v = self.resolveArchivedPosition(key, root_v)
1261 if v:
1262 v.unknownAttributes = resultDict[key]
1263 v._p_changed = True
1264 expanded, marks = {}, {}
1265 for gnx in self.descendentExpandedList:
1266 v = self.gnxDict.get(gnx)
1267 if v:
1268 expanded[v] = v
1269 for gnx in self.descendentMarksList:
1270 v = self.gnxDict.get(gnx)
1271 if v:
1272 marks[v] = v
1273 if marks or expanded:
1274 for p in c.all_unique_positions():
1275 if marks.get(p.v):
1276 p.v.initMarkedBit()
1277 # This was the problem: was p.setMark.
1278 # There was a big performance bug in the mark hook in the Node Navigator plugin.
1279 if expanded.get(p.v):
1280 p.expand()
1281 #@+node:ekr.20060919110638.13: *5* fc.setPositionsFromVnodes
1282 def setPositionsFromVnodes(self):
1284 c, root = self.c, self.c.rootPosition()
1285 if c.sqlite_connection:
1286 # position is already selected
1287 return
1288 current, str_pos = None, None
1289 if c.mFileName:
1290 str_pos = c.db.get('current_position')
1291 if str_pos is None:
1292 d = root.v.u
1293 if d:
1294 str_pos = d.get('str_leo_pos')
1295 if str_pos is not None:
1296 current = self.archivedPositionToPosition(str_pos)
1297 c.setCurrentPosition(current or c.rootPosition())
1298 #@+node:ekr.20031218072017.3032: *3* fc: Writing
1299 #@+node:ekr.20070413045221.2: *4* fc: Writing save*
1300 #@+node:ekr.20031218072017.1720: *5* fc.save
1301 def save(self, fileName, silent=False):
1302 """fc.save: A helper for c.save."""
1303 c = self.c
1304 p = c.p
1305 # New in 4.2. Return ok flag so shutdown logic knows if all went well.
1306 ok = g.doHook("save1", c=c, p=p, fileName=fileName)
1307 if ok is None:
1308 c.endEditing() # Set the current headline text.
1309 self.setDefaultDirectoryForNewFiles(fileName)
1310 g.app.commander_cacher.save(c, fileName)
1311 ok = c.checkFileTimeStamp(fileName)
1312 if ok:
1313 if c.sqlite_connection:
1314 c.sqlite_connection.close()
1315 c.sqlite_connection = None
1316 ok = self.write_Leo_file(fileName)
1317 if ok:
1318 if not silent:
1319 self.putSavedMessage(fileName)
1320 c.clearChanged() # Clears all dirty bits.
1321 if c.config.save_clears_undo_buffer:
1322 g.es("clearing undo")
1323 c.undoer.clearUndoState()
1324 c.redraw_after_icons_changed()
1325 g.doHook("save2", c=c, p=p, fileName=fileName)
1326 return ok
1327 #@+node:vitalije.20170831135146.1: *5* fc.save_ref & helpers
1328 def save_ref(self):
1329 """Saves reference outline file"""
1330 c = self.c
1331 p = c.p
1332 fc = self
1333 #@+others
1334 #@+node:vitalije.20170831135535.1: *6* function: put_v_elements
1335 def put_v_elements():
1336 """
1337 Puts all <v> elements in the order in which they appear in the outline.
1339 This is not the same as fc.put_v_elements!
1340 """
1341 c.clearAllVisited()
1342 fc.put("<vnodes>\n")
1343 # Make only one copy for all calls.
1344 fc.currentPosition = c.p
1345 fc.rootPosition = c.rootPosition()
1346 fc.vnodesDict = {}
1347 ref_fname = None
1348 for p in c.rootPosition().self_and_siblings(copy=False):
1349 if p.h == PRIVAREA:
1350 ref_fname = p.b.split('\n', 1)[0].strip()
1351 break
1352 # An optimization: Write the next top-level node.
1353 fc.put_v_element(p, isIgnore=p.isAtIgnoreNode())
1354 fc.put("</vnodes>\n")
1355 return ref_fname
1356 #@+node:vitalije.20170831135447.1: *6* function: getPublicLeoFile
1357 def getPublicLeoFile():
1358 fc.outputFile = io.StringIO()
1359 fc.putProlog()
1360 fc.putHeader()
1361 fc.putGlobals()
1362 fc.putPrefs()
1363 fc.putFindSettings()
1364 fname = put_v_elements()
1365 put_t_elements()
1366 fc.putPostlog()
1367 return fname, fc.outputFile.getvalue()
1369 #@+node:vitalije.20211218225014.1: *6* function: put_t_elements
1370 def put_t_elements():
1371 """
1372 Write all <t> elements except those for vnodes appearing in @file, @edit or @auto nodes.
1373 """
1375 def should_suppress(p):
1376 return any(z.isAtFileNode() or z.isAtEditNode() or z.isAtAutoNode()
1377 for z in p.self_and_parents())
1379 fc.put("<tnodes>\n")
1380 suppress = {}
1381 for p in c.all_positions(copy=False):
1382 if should_suppress(p):
1383 suppress[p.v] = True
1385 toBeWritten = {}
1386 for root in c.rootPosition().self_and_siblings():
1387 if root.h == PRIVAREA:
1388 break
1389 for p in root.self_and_subtree():
1390 if p.v not in suppress and p.v not in toBeWritten:
1391 toBeWritten[p.v.fileIndex] = p.v
1392 for gnx in sorted(toBeWritten):
1393 v = toBeWritten[gnx]
1394 fc.put_t_element(v)
1395 fc.put("</tnodes>\n")
1396 #@-others
1397 c.endEditing()
1398 for v in c.hiddenRootNode.children:
1399 if v.h == PRIVAREA:
1400 fileName = g.splitLines(v.b)[0].strip()
1401 break
1402 else:
1403 fileName = c.mFileName
1404 # New in 4.2. Return ok flag so shutdown logic knows if all went well.
1405 ok = g.doHook("save1", c=c, p=p, fileName=fileName)
1406 if ok is None:
1407 fileName, content = getPublicLeoFile()
1408 fileName = g.os_path_finalize_join(c.openDirectory, fileName)
1409 with open(fileName, 'w', encoding="utf-8", newline='\n') as out:
1410 out.write(content)
1411 g.es('updated reference file:',
1412 g.shortFileName(fileName))
1413 g.doHook("save2", c=c, p=p, fileName=fileName)
1414 return ok
1415 #@+node:ekr.20031218072017.3043: *5* fc.saveAs
1416 def saveAs(self, fileName):
1417 """fc.saveAs: A helper for c.saveAs."""
1418 c = self.c
1419 p = c.p
1420 if not g.doHook("save1", c=c, p=p, fileName=fileName):
1421 c.endEditing() # Set the current headline text.
1422 if c.sqlite_connection:
1423 c.sqlite_connection.close()
1424 c.sqlite_connection = None
1425 self.setDefaultDirectoryForNewFiles(fileName)
1426 g.app.commander_cacher.save(c, fileName)
1427 # Disable path-changed messages in writeAllHelper.
1428 c.ignoreChangedPaths = True
1429 try:
1430 if self.write_Leo_file(fileName):
1431 c.clearChanged() # Clears all dirty bits.
1432 self.putSavedMessage(fileName)
1433 finally:
1434 c.ignoreChangedPaths = False # #1367.
1435 c.redraw_after_icons_changed()
1436 g.doHook("save2", c=c, p=p, fileName=fileName)
1437 #@+node:ekr.20031218072017.3044: *5* fc.saveTo
1438 def saveTo(self, fileName, silent=False):
1439 """fc.saveTo: A helper for c.saveTo."""
1440 c = self.c
1441 p = c.p
1442 if not g.doHook("save1", c=c, p=p, fileName=fileName):
1443 c.endEditing() # Set the current headline text.
1444 if c.sqlite_connection:
1445 c.sqlite_connection.close()
1446 c.sqlite_connection = None
1447 self.setDefaultDirectoryForNewFiles(fileName)
1448 g.app.commander_cacher.commit() # Commit, but don't save file name.
1449 # Disable path-changed messages in writeAllHelper.
1450 c.ignoreChangedPaths = True
1451 try:
1452 self.write_Leo_file(fileName)
1453 finally:
1454 c.ignoreChangedPaths = False
1455 if not silent:
1456 self.putSavedMessage(fileName)
1457 c.redraw_after_icons_changed()
1458 g.doHook("save2", c=c, p=p, fileName=fileName)
1459 #@+node:ekr.20210316034237.1: *4* fc: Writing top-level
1460 #@+node:vitalije.20170630172118.1: *5* fc.exportToSqlite & helpers
1461 def exportToSqlite(self, fileName):
1462 """Dump all vnodes to sqlite database. Returns True on success."""
1463 c, fc = self.c, self
1464 if c.sqlite_connection is None:
1465 c.sqlite_connection = sqlite3.connect(fileName, isolation_level='DEFERRED')
1466 conn = c.sqlite_connection
1468 def dump_u(v) -> bytes:
1469 try:
1470 s = pickle.dumps(v.u, protocol=1)
1471 except pickle.PicklingError:
1472 s = b'' # 2021/06/25: fixed via mypy complaint.
1473 g.trace('unpickleable value', repr(v.u))
1474 return s
1476 dbrow = lambda v: (
1477 v.gnx,
1478 v.h,
1479 v.b,
1480 ' '.join(x.gnx for x in v.children),
1481 ' '.join(x.gnx for x in v.parents),
1482 v.iconVal,
1483 v.statusBits,
1484 dump_u(v)
1485 )
1486 ok = False
1487 try:
1488 fc.prepareDbTables(conn)
1489 fc.exportDbVersion(conn)
1490 fc.exportVnodesToSqlite(conn, (dbrow(v) for v in c.all_unique_nodes()))
1491 fc.exportGeomToSqlite(conn)
1492 fc.exportHashesToSqlite(conn)
1493 conn.commit()
1494 ok = True
1495 except sqlite3.Error as e:
1496 g.internalError(e)
1497 return ok
1498 #@+node:vitalije.20170705075107.1: *6* fc.decodePosition
1499 def decodePosition(self, s):
1500 """Creates position from its string representation encoded by fc.encodePosition."""
1501 fc = self
1502 if not s:
1503 return fc.c.rootPosition()
1504 sep = '<->'
1505 comma = ','
1506 stack = [x.split(comma) for x in s.split(sep)]
1507 stack = [(fc.gnxDict[x], int(y)) for x, y in stack]
1508 v, ci = stack[-1]
1509 p = leoNodes.Position(v, ci, stack[:-1])
1510 return p
1511 #@+node:vitalije.20170705075117.1: *6* fc.encodePosition
1512 def encodePosition(self, p):
1513 """New schema for encoding current position hopefully simplier one."""
1514 jn = '<->'
1515 mk = '%s,%s'
1516 res = [mk % (x.gnx, y) for x, y in p.stack]
1517 res.append(mk % (p.gnx, p._childIndex))
1518 return jn.join(res)
1519 #@+node:vitalije.20170811130512.1: *6* fc.prepareDbTables
1520 def prepareDbTables(self, conn):
1521 conn.execute('''drop table if exists vnodes;''')
1522 conn.execute(
1523 '''
1524 create table if not exists vnodes(
1525 gnx primary key,
1526 head,
1527 body,
1528 children,
1529 parents,
1530 iconVal,
1531 statusBits,
1532 ua);''',
1533 )
1534 conn.execute(
1535 '''create table if not exists extra_infos(name primary key, value)''')
1536 #@+node:vitalije.20170701161851.1: *6* fc.exportVnodesToSqlite
1537 def exportVnodesToSqlite(self, conn, rows):
1538 conn.executemany(
1539 '''insert into vnodes
1540 (gnx, head, body, children, parents,
1541 iconVal, statusBits, ua)
1542 values(?,?,?,?,?,?,?,?);''',
1543 rows,
1544 )
1545 #@+node:vitalije.20170701162052.1: *6* fc.exportGeomToSqlite
1546 def exportGeomToSqlite(self, conn):
1547 c = self.c
1548 data = zip(
1549 (
1550 'width', 'height', 'left', 'top',
1551 'ratio', 'secondary_ratio',
1552 'current_position'
1553 ),
1554 c.frame.get_window_info() +
1555 (
1556 c.frame.ratio, c.frame.secondary_ratio,
1557 self.encodePosition(c.p)
1558 )
1559 )
1560 conn.executemany('replace into extra_infos(name, value) values(?, ?)', data)
1561 #@+node:vitalije.20170811130559.1: *6* fc.exportDbVersion
1562 def exportDbVersion(self, conn):
1563 conn.execute(
1564 "replace into extra_infos(name, value) values('dbversion', ?)", ('1.0',))
1565 #@+node:vitalije.20170701162204.1: *6* fc.exportHashesToSqlite
1566 def exportHashesToSqlite(self, conn):
1567 c = self.c
1569 def md5(x):
1570 try:
1571 s = open(x, 'rb').read()
1572 except Exception:
1573 return ''
1574 s = s.replace(b'\r\n', b'\n')
1575 return hashlib.md5(s).hexdigest()
1577 files = set()
1579 p = c.rootPosition()
1580 while p:
1581 if p.isAtIgnoreNode():
1582 p.moveToNodeAfterTree()
1583 elif p.isAtAutoNode() or p.isAtFileNode():
1584 fn = c.getNodeFileName(p)
1585 files.add((fn, 'md5_' + p.gnx))
1586 p.moveToNodeAfterTree()
1587 else:
1588 p.moveToThreadNext()
1589 conn.executemany(
1590 'replace into extra_infos(name, value) values(?,?)',
1591 map(lambda x: (x[1], md5(x[0])), files))
1592 #@+node:ekr.20031218072017.1573: *5* fc.outline_to_clipboard_string
1593 def outline_to_clipboard_string(self, p=None):
1594 """
1595 Return a string suitable for pasting to the clipboard.
1596 """
1597 try:
1598 # Save
1599 tua = self.descendentTnodeUaDictList
1600 vua = self.descendentVnodeUaDictList
1601 gnxDict = self.gnxDict
1602 vnodesDict = self.vnodesDict
1603 # Paste.
1604 self.outputFile = io.StringIO()
1605 self.usingClipboard = True
1606 self.putProlog()
1607 self.putHeader()
1608 self.put_v_elements(p or self.c.p)
1609 self.put_t_elements()
1610 self.putPostlog()
1611 s = self.outputFile.getvalue()
1612 self.outputFile = None
1613 finally:
1614 # Restore
1615 self.descendentTnodeUaDictList = tua
1616 self.descendentVnodeUaDictList = vua
1617 self.gnxDict = gnxDict
1618 self.vnodesDict = vnodesDict
1619 self.usingClipboard = False
1620 return s
1621 #@+node:ekr.20040324080819.1: *5* fc.outline_to_xml_string
1622 def outline_to_xml_string(self):
1623 """Write the outline in .leo (XML) format to a string."""
1624 self.outputFile = io.StringIO()
1625 self.putProlog()
1626 self.putHeader()
1627 self.putGlobals()
1628 self.putPrefs()
1629 self.putFindSettings()
1630 self.put_v_elements()
1631 self.put_t_elements()
1632 self.putPostlog()
1633 s = self.outputFile.getvalue()
1634 self.outputFile = None
1635 return s
1636 #@+node:ekr.20031218072017.3046: *5* fc.write_Leo_file
1637 def write_Leo_file(self, fileName):
1638 """
1639 Write all external files and the .leo file itself."""
1640 c, fc = self.c, self
1641 if c.checkOutline():
1642 g.error('Structural errors in outline! outline not written')
1643 return False
1644 g.app.recentFilesManager.writeRecentFilesFile(c)
1645 fc.writeAllAtFileNodes() # Ignore any errors.
1646 return fc.writeOutline(fileName)
1648 write_LEO_file = write_Leo_file # For compatibility with old plugins.
1649 #@+node:ekr.20210316050301.1: *5* fc.write_leojs & helpers
1650 def write_leojs(self, fileName):
1651 """Write the outline in .leojs (JSON) format."""
1652 c = self.c
1653 ok, backupName = self.createBackupFile(fileName)
1654 if not ok:
1655 return False
1656 f = self.openOutlineForWriting(fileName)
1657 if not f:
1658 return False
1659 try:
1660 # Create the dict corresponding to the JSON.
1661 d = self.leojs_file()
1662 # Convert the dict to JSON.
1663 json_s = json.dumps(d, indent=2)
1664 s = bytes(json_s, self.leo_file_encoding, 'replace')
1665 f.write(s)
1666 f.close()
1667 g.app.commander_cacher.save(c, fileName)
1668 c.setFileTimeStamp(fileName)
1669 # Delete backup file.
1670 if backupName and g.os_path_exists(backupName):
1671 self.deleteBackupFile(backupName)
1672 self.mFileName = fileName
1673 return True
1674 except Exception:
1675 self.handleWriteLeoFileException(fileName, backupName, f)
1676 return False
1677 #@+node:ekr.20210316095706.1: *6* fc.leojs_file
1678 def leojs_file(self):
1679 """Return a dict representing the outline."""
1680 c = self.c
1681 return {
1682 'leoHeader': {'fileFormat': 2},
1683 'globals': self.leojs_globals(),
1684 'tnodes': {v.gnx: v._bodyString for v in c.all_unique_nodes()},
1685 # 'tnodes': [
1686 # {
1687 # 'tx': v.fileIndex,
1688 # 'body': v._bodyString,
1689 # } for v in c.all_unique_nodes()
1690 # ],
1691 'vnodes': [
1692 self.leojs_vnode(p.v) for p in c.rootPosition().self_and_siblings()
1693 ],
1694 }
1695 #@+node:ekr.20210316092313.1: *6* fc.leojs_globals (sets window_position)
1696 def leojs_globals(self):
1697 """Put json representation of Leo's cached globals."""
1698 c = self.c
1699 width, height, left, top = c.frame.get_window_info()
1700 if 1: # Write to the cache, not the file.
1701 d: Dict[str, str] = {}
1702 c.db['body_outline_ratio'] = str(c.frame.ratio)
1703 c.db['body_secondary_ratio'] = str(c.frame.secondary_ratio)
1704 c.db['window_position'] = str(top), str(left), str(height), str(width)
1705 if 'size' in g.app.debug:
1706 g.trace('set window_position:', c.db['window_position'], c.shortFileName())
1707 else:
1708 d = {
1709 'body_outline_ratio': c.frame.ratio,
1710 'body_secondary_ratio': c.frame.secondary_ratio,
1711 'globalWindowPosition': {
1712 'top': top,
1713 'left': left,
1714 'width': width,
1715 'height': height,
1716 },
1717 }
1718 return d
1719 #@+node:ekr.20210316085413.2: *6* fc.leojs_vnodes
1720 def leojs_vnode(self, v):
1721 """Return a jsonized vnode."""
1722 return {
1723 'gnx': v.fileIndex,
1724 'vh': v._headString,
1725 'status': v.statusBits,
1726 'children': [self.leojs_vnode(child) for child in v.children]
1727 }
1728 #@+node:ekr.20100119145629.6111: *5* fc.write_xml_file
1729 def write_xml_file(self, fileName):
1730 """Write the outline in .leo (XML) format."""
1731 c = self.c
1732 ok, backupName = self.createBackupFile(fileName)
1733 if not ok:
1734 return False
1735 f = self.openOutlineForWriting(fileName)
1736 if not f:
1737 return False
1738 self.mFileName = fileName
1739 try:
1740 s = self.outline_to_xml_string()
1741 s = bytes(s, self.leo_file_encoding, 'replace')
1742 f.write(s)
1743 f.close()
1744 c.setFileTimeStamp(fileName)
1745 # Delete backup file.
1746 if backupName and g.os_path_exists(backupName):
1747 self.deleteBackupFile(backupName)
1748 return True
1749 except Exception:
1750 self.handleWriteLeoFileException(fileName, backupName, f)
1751 return False
1752 #@+node:ekr.20100119145629.6114: *5* fc.writeAllAtFileNodes
1753 def writeAllAtFileNodes(self):
1754 """Write all @<file> nodes and set orphan bits."""
1755 c = self.c
1756 try:
1757 # To allow Leo to quit properly, do *not* signal failure here.
1758 c.atFileCommands.writeAll(all=False)
1759 return True
1760 except Exception:
1761 # #1260415: https://bugs.launchpad.net/leo-editor/+bug/1260415
1762 g.es_error("exception writing external files")
1763 g.es_exception()
1764 g.es('Internal error writing one or more external files.', color='red')
1765 g.es('Please report this error to:', color='blue')
1766 g.es('https://groups.google.com/forum/#!forum/leo-editor', color='blue')
1767 g.es('All changes will be lost unless you', color='red')
1768 g.es('can save each changed file.', color='red')
1769 return False
1770 #@+node:ekr.20210316041806.1: *5* fc.writeOutline (write switch)
1771 def writeOutline(self, fileName):
1773 c = self.c
1774 if c.checkOutline():
1775 g.error('Structure errors in outline! outline not written')
1776 return False
1777 if self.isReadOnly(fileName):
1778 return False
1779 if fileName.endswith('.db'):
1780 return self.exportToSqlite(fileName)
1781 if fileName.endswith('.leojs'):
1782 return self.write_leojs(fileName)
1783 return self.write_xml_file(fileName)
1784 #@+node:ekr.20070412095520: *5* fc.writeZipFile
1785 def writeZipFile(self, s):
1786 """Write string s as a .zip file."""
1787 # The name of the file in the archive.
1788 contentsName = g.toEncodedString(
1789 g.shortFileName(self.mFileName),
1790 self.leo_file_encoding, reportErrors=True)
1791 # The name of the archive itself.
1792 fileName = g.toEncodedString(
1793 self.mFileName,
1794 self.leo_file_encoding, reportErrors=True)
1795 # Write the archive.
1796 # These mypy complaints look valid.
1797 theFile = zipfile.ZipFile(fileName, 'w', zipfile.ZIP_DEFLATED) # type:ignore
1798 theFile.writestr(contentsName, s) # type:ignore
1799 theFile.close()
1800 #@+node:ekr.20210316034532.1: *4* fc.Writing Utils
1801 #@+node:ekr.20080805085257.2: *5* fc.pickle
1802 def pickle(self, torv, val, tag):
1803 """Pickle val and return the hexlified result."""
1804 try:
1805 s = pickle.dumps(val, protocol=1)
1806 s2 = binascii.hexlify(s)
1807 s3 = g.toUnicode(s2, 'utf-8')
1808 field = f' {tag}="{s3}"'
1809 return field
1810 except pickle.PicklingError:
1811 if tag: # The caller will print the error if tag is None.
1812 g.warning("ignoring non-pickleable value", val, "in", torv)
1813 return ''
1814 except Exception:
1815 g.error("fc.pickle: unexpected exception in", torv)
1816 g.es_exception()
1817 return ''
1818 #@+node:ekr.20031218072017.1470: *5* fc.put
1819 def put(self, s):
1820 """Put string s to self.outputFile. All output eventually comes here."""
1821 if s:
1822 self.outputFile.write(s)
1823 #@+node:ekr.20080805071954.2: *5* fc.putDescendentVnodeUas & helper
1824 def putDescendentVnodeUas(self, p):
1825 """
1826 Return the a uA field for descendent VNode attributes,
1827 suitable for reconstituting uA's for anonymous vnodes.
1828 """
1829 #
1830 # Create aList of tuples (p,v) having a valid unknownAttributes dict.
1831 # Create dictionary: keys are vnodes, values are corresonding archived positions.
1832 aList = []
1833 pDict = {}
1834 for p2 in p.self_and_subtree(copy=False):
1835 if hasattr(p2.v, "unknownAttributes"):
1836 aList.append((p2.copy(), p2.v),)
1837 pDict[p2.v] = p2.archivedPosition(root_p=p)
1838 # Create aList of pairs (v,d) where d contains only pickleable entries.
1839 if aList:
1840 aList = self.createUaList(aList)
1841 if not aList:
1842 return ''
1843 # Create d, an enclosing dict to hold all the inner dicts.
1844 d = {}
1845 for v, d2 in aList:
1846 aList2 = [str(z) for z in pDict.get(v)]
1847 key = '.'.join(aList2)
1848 d[key] = d2
1849 # Pickle and hexlify d
1850 # pylint: disable=consider-using-ternary
1851 return d and self.pickle(
1852 torv=p.v, val=d, tag='descendentVnodeUnknownAttributes') or ''
1853 #@+node:ekr.20080805085257.1: *6* fc.createUaList
1854 def createUaList(self, aList):
1855 """
1856 Given aList of pairs (p,torv), return a list of pairs (torv,d)
1857 where d contains all picklable items of torv.unknownAttributes.
1858 """
1859 result = []
1860 for p, torv in aList:
1861 if isinstance(torv.unknownAttributes, dict):
1862 # Create a new dict containing only entries that can be pickled.
1863 d = dict(torv.unknownAttributes) # Copy the dict.
1864 for key in d:
1865 # Just see if val can be pickled. Suppress any error.
1866 ok = self.pickle(torv=torv, val=d.get(key), tag=None)
1867 if not ok:
1868 del d[key]
1869 g.warning("ignoring bad unknownAttributes key", key, "in", p.h)
1870 if d:
1871 result.append((torv, d),)
1872 else:
1873 g.warning("ignoring non-dictionary uA for", p)
1874 return result
1875 #@+node:ekr.20031218072017.3035: *5* fc.putFindSettings
1876 def putFindSettings(self):
1877 # New in 4.3: These settings never get written to the .leo file.
1878 self.put("<find_panel_settings/>\n")
1879 #@+node:ekr.20031218072017.3037: *5* fc.putGlobals (sets window_position)
1880 def putGlobals(self):
1881 """Put a vestigial <globals> element, and write global data to the cache."""
1882 trace = 'cache' in g.app.debug
1883 c = self.c
1884 self.put("<globals/>\n")
1885 if not c.mFileName:
1886 return
1887 c.db['body_outline_ratio'] = str(c.frame.ratio)
1888 c.db['body_secondary_ratio'] = str(c.frame.secondary_ratio)
1889 w, h, l, t = c.frame.get_window_info()
1890 c.db['window_position'] = str(t), str(l), str(h), str(w)
1891 if trace:
1892 g.trace(f"\nset c.db for {c.shortFileName()}")
1893 print('window_position:', c.db['window_position'])
1894 #@+node:ekr.20031218072017.3041: *5* fc.putHeader
1895 def putHeader(self):
1896 self.put('<leo_header file_format="2"/>\n')
1897 #@+node:ekr.20031218072017.3042: *5* fc.putPostlog
1898 def putPostlog(self):
1899 self.put("</leo_file>\n")
1900 #@+node:ekr.20031218072017.2066: *5* fc.putPrefs
1901 def putPrefs(self):
1902 # New in 4.3: These settings never get written to the .leo file.
1903 self.put("<preferences/>\n")
1904 #@+node:ekr.20031218072017.1246: *5* fc.putProlog
1905 def putProlog(self):
1906 """Put the prolog of the xml file."""
1907 tag = 'http://leoeditor.com/namespaces/leo-python-editor/1.1'
1908 self.putXMLLine()
1909 # Put "created by Leo" line.
1910 self.put('<!-- Created by Leo: http://leoeditor.com/leo_toc.html -->\n')
1911 self.putStyleSheetLine()
1912 # Put the namespace
1913 self.put(f'<leo_file xmlns:leo="{tag}" >\n')
1914 #@+node:ekr.20070413061552: *5* fc.putSavedMessage
1915 def putSavedMessage(self, fileName):
1916 c = self.c
1917 # #531: Optionally report timestamp...
1918 if c.config.getBool('log-show-save-time', default=False):
1919 format = c.config.getString('log-timestamp-format') or "%H:%M:%S"
1920 timestamp = time.strftime(format) + ' '
1921 else:
1922 timestamp = ''
1923 g.es(f"{timestamp}saved: {g.shortFileName(fileName)}")
1924 #@+node:ekr.20031218072017.1248: *5* fc.putStyleSheetLine
1925 def putStyleSheetLine(self):
1926 """
1927 Put the xml stylesheet line.
1929 Leo 5.3:
1930 - Use only the stylesheet setting, ignoreing c.frame.stylesheet.
1931 - Write no stylesheet element if there is no setting.
1933 The old way made it almost impossible to delete stylesheet element.
1934 """
1935 c = self.c
1936 sheet = (c.config.getString('stylesheet') or '').strip()
1937 # sheet2 = c.frame.stylesheet and c.frame.stylesheet.strip() or ''
1938 # sheet = sheet or sheet2
1939 if sheet:
1940 self.put(f"<?xml-stylesheet {sheet} ?>\n")
1942 #@+node:ekr.20031218072017.1577: *5* fc.put_t_element
1943 def put_t_element(self, v):
1944 b, gnx = v.b, v.fileIndex
1945 ua = self.putUnknownAttributes(v)
1946 body = xml.sax.saxutils.escape(b) if b else ''
1947 self.put(f'<t tx="{gnx}"{ua}>{body}</t>\n')
1948 #@+node:ekr.20031218072017.1575: *5* fc.put_t_elements
1949 def put_t_elements(self):
1950 """Put all <t> elements as required for copy or save commands"""
1951 self.put("<tnodes>\n")
1952 self.putReferencedTElements()
1953 self.put("</tnodes>\n")
1954 #@+node:ekr.20031218072017.1576: *6* fc.putReferencedTElements
1955 def putReferencedTElements(self):
1956 """Put <t> elements for all referenced vnodes."""
1957 c = self.c
1958 if self.usingClipboard: # write the current tree.
1959 theIter = self.currentPosition.self_and_subtree(copy=False)
1960 else: # write everything
1961 theIter = c.all_unique_positions(copy=False)
1962 # Populate the vnodes dict.
1963 vnodes = {}
1964 for p in theIter:
1965 # Make *sure* the file index has the proper form.
1966 # pylint: disable=unbalanced-tuple-unpacking
1967 index = p.v.fileIndex
1968 vnodes[index] = p.v
1969 # Put all vnodes in index order.
1970 for index in sorted(vnodes):
1971 v = vnodes.get(index)
1972 if v:
1973 # Write <t> elements only for vnodes that will be written.
1974 # For example, vnodes in external files will be written
1975 # only if the vnodes are cloned outside the file.
1976 if v.isWriteBit():
1977 self.put_t_element(v)
1978 else:
1979 g.trace('can not happen: no VNode for', repr(index))
1980 # This prevents the file from being written.
1981 raise BadLeoFile(f"no VNode for {repr(index)}")
1982 #@+node:ekr.20050418161620.2: *5* fc.putUaHelper
1983 def putUaHelper(self, torv, key, val):
1984 """Put attribute whose name is key and value is val to the output stream."""
1985 # New in 4.3: leave string attributes starting with 'str_' alone.
1986 if key.startswith('str_'):
1987 if isinstance(val, (str, bytes)):
1988 val = g.toUnicode(val)
1989 attr = f' {key}="{xml.sax.saxutils.escape(val)}"'
1990 return attr
1991 g.trace(type(val), repr(val))
1992 g.warning("ignoring non-string attribute", key, "in", torv)
1993 return ''
1994 return self.pickle(torv=torv, val=val, tag=key)
1995 #@+node:EKR.20040526202501: *5* fc.putUnknownAttributes
1996 def putUnknownAttributes(self, v):
1997 """Put pickleable values for all keys in v.unknownAttributes dictionary."""
1998 if not hasattr(v, 'unknownAttributes'):
1999 return ''
2000 attrDict = v.unknownAttributes
2001 if isinstance(attrDict, dict):
2002 val = ''.join(
2003 [self.putUaHelper(v, key, val)
2004 for key, val in attrDict.items()])
2005 return val
2006 g.warning("ignoring non-dictionary unknownAttributes for", v)
2007 return ''
2008 #@+node:ekr.20031218072017.1863: *5* fc.put_v_element & helper
2009 def put_v_element(self, p, isIgnore=False):
2010 """Write a <v> element corresponding to a VNode."""
2011 fc = self
2012 v = p.v
2013 #
2014 # Precompute constants.
2015 isAuto = p.isAtAutoNode() and p.atAutoNodeName().strip()
2016 isEdit = p.isAtEditNode() and p.atEditNodeName().strip() and not p.hasChildren()
2017 # Write the entire @edit tree if it has children.
2018 isFile = p.isAtFileNode()
2019 isShadow = p.isAtShadowFileNode()
2020 isThin = p.isAtThinFileNode()
2021 #
2022 # Set forcewrite.
2023 if isIgnore or p.isAtIgnoreNode():
2024 forceWrite = True
2025 elif isAuto or isEdit or isFile or isShadow or isThin:
2026 forceWrite = False
2027 else:
2028 forceWrite = True
2029 #
2030 # Set the write bit if necessary.
2031 gnx = v.fileIndex
2032 if forceWrite or self.usingClipboard:
2033 v.setWriteBit() # 4.2: Indicate we wrote the body text.
2035 attrs = fc.compute_attribute_bits(forceWrite, p)
2036 #
2037 # Write the node.
2038 v_head = f'<v t="{gnx}"{attrs}>'
2039 if gnx in fc.vnodesDict:
2040 fc.put(v_head + '</v>\n')
2041 else:
2042 fc.vnodesDict[gnx] = True
2043 v_head += f"<vh>{xml.sax.saxutils.escape(p.v.headString() or '')}</vh>"
2044 # New in 4.2: don't write child nodes of @file-thin trees
2045 # (except when writing to clipboard)
2046 if p.hasChildren() and (forceWrite or self.usingClipboard):
2047 fc.put(f"{v_head}\n")
2048 # This optimization eliminates all "recursive" copies.
2049 p.moveToFirstChild()
2050 while 1:
2051 fc.put_v_element(p, isIgnore)
2052 if p.hasNext():
2053 p.moveToNext()
2054 else:
2055 break
2056 p.moveToParent() # Restore p in the caller.
2057 fc.put('</v>\n')
2058 else:
2059 fc.put(f"{v_head}</v>\n") # Call put only once.
2060 #@+node:ekr.20031218072017.1865: *6* fc.compute_attribute_bits
2061 def compute_attribute_bits(self, forceWrite, p):
2062 """Return the initial values of v's attributes."""
2063 attrs = []
2064 if p.hasChildren() and not forceWrite and not self.usingClipboard:
2065 # Fix #526: do this for @auto nodes as well.
2066 attrs.append(self.putDescendentVnodeUas(p))
2067 return ''.join(attrs)
2068 #@+node:ekr.20031218072017.1579: *5* fc.put_v_elements & helper
2069 def put_v_elements(self, p=None):
2070 """Puts all <v> elements in the order in which they appear in the outline."""
2071 c = self.c
2072 c.clearAllVisited()
2073 self.put("<vnodes>\n")
2074 # Make only one copy for all calls.
2075 self.currentPosition = p or c.p
2076 self.rootPosition = c.rootPosition()
2077 self.vnodesDict = {}
2078 if self.usingClipboard:
2079 self.expanded_gnxs, self.marked_gnxs = set(), set()
2080 # These will be ignored.
2081 self.put_v_element(self.currentPosition)
2082 # Write only current tree.
2083 else:
2084 for p in c.rootPosition().self_and_siblings():
2085 self.put_v_element(p, isIgnore=p.isAtIgnoreNode())
2086 # Fix #1018: scan *all* nodes.
2087 self.setCachedBits()
2088 self.put("</vnodes>\n")
2089 #@+node:ekr.20190328160622.1: *6* fc.setCachedBits
2090 def setCachedBits(self):
2091 """
2092 Set the cached expanded and marked bits for *all* nodes.
2093 Also cache the current position.
2094 """
2095 trace = 'cache' in g.app.debug
2096 c = self.c
2097 if not c.mFileName:
2098 return # New.
2099 current = [str(z) for z in self.currentPosition.archivedPosition()]
2100 expanded = [v.gnx for v in c.all_unique_nodes() if v.isExpanded()]
2101 marked = [v.gnx for v in c.all_unique_nodes() if v.isMarked()]
2102 c.db['expanded'] = ','.join(expanded)
2103 c.db['marked'] = ','.join(marked)
2104 c.db['current_position'] = ','.join(current)
2105 if trace:
2106 g.trace(f"\nset c.db for {c.shortFileName()}")
2107 print('expanded:', expanded)
2108 print('marked:', marked)
2109 print('current_position:', current)
2110 print('')
2111 #@+node:ekr.20031218072017.1247: *5* fc.putXMLLine
2112 def putXMLLine(self):
2113 """Put the **properly encoded** <?xml> element."""
2114 # Use self.leo_file_encoding encoding.
2115 self.put(
2116 f"{g.app.prolog_prefix_string}"
2117 f'"{self.leo_file_encoding}"'
2118 f"{g.app.prolog_postfix_string}\n")
2119 #@-others
2120#@-others
2121#@@language python
2122#@@tabwidth -4
2123#@@pagewidth 70
2124#@-leo