~jaap.karssenberg/zim/pyzim-0.43-backports

99 by Jaap Karssenberg
include gettext and "utf8" -> "utf-8"
1
# -*- coding: utf-8 -*-
6 by Jaap Karssenberg
- Added mostly workign template class
2
3
# Copyright 2008 Jaap Karssenberg <pardus@cpan.org>
3 by Jaap Karssenberg
sync
4
4 by Jaap Karssenberg
Added classes Notebook, Page and PageList
5
'''
59 by Jaap Karssenberg
Refactored notebook API around db index - tests OK, GUI still broken
6
This package contains the main Notebook class and related classes.
4 by Jaap Karssenberg
Added classes Notebook, Page and PageList
7
8
This package defines the public interface towards the
9
noetbook.  As a backend it uses one of more packages from
10
the 'stores' namespace.
11
'''
12
162 by Jaap Karssenberg
Various fixes and manual updates
13
from __future__ import with_statement
14
83 by pardus
sync
15
import os
3 by Jaap Karssenberg
sync
16
import weakref
62 by Jaap Karssenberg
index is now actually saved
17
import logging
3 by Jaap Karssenberg
sync
18
89 by pardus
Basic move & delete page implemented
19
import gobject
20
154.1.2 by Jaap Karssenberg
Reworked the code for maintaining the notebook list
21
import zim.fs
9 by Jaap Karssenberg
Added File and Dir objects
22
from zim.fs import *
162 by Jaap Karssenberg
Various fixes and manual updates
23
from zim.errors import Error, SignalExceptionContext, SignalRaiseExceptionContext
154.1.2 by Jaap Karssenberg
Reworked the code for maintaining the notebook list
24
from zim.config import ConfigDict, ConfigDictFile, TextConfigFile, HierarchicDict, \
121.1.6 by Jaap Karssenberg
Added special template for Calendar pages
25
	config_file, data_dir, user_dirs
175 by Jaap Karssenberg
* Added interwiki support
26
from zim.parsing import Re, is_url_re, is_email_re, is_win32_path_re, link_type, url_encode
9 by Jaap Karssenberg
Added File and Dir objects
27
import zim.stores
58.1.1 by Jaap Karssenberg
integrated pathbar widget
28
59 by Jaap Karssenberg
Refactored notebook API around db index - tests OK, GUI still broken
29
62 by Jaap Karssenberg
index is now actually saved
30
logger = logging.getLogger('zim.notebook')
31
32
154.1.2 by Jaap Karssenberg
Reworked the code for maintaining the notebook list
33
class NotebookList(TextConfigFile):
34
	'''This class keeps a list of paths for notebook locations
35
	plus a attribute 'default' for the default notebook.
121.1.4 by Jaap Karssenberg
Can now open pages by filename from the command line
36
154.1.2 by Jaap Karssenberg
Reworked the code for maintaining the notebook list
37
	All values are assumed to be (file://) urls.
121.1.4 by Jaap Karssenberg
Can now open pages by filename from the command line
38
	'''
154.1.2 by Jaap Karssenberg
Reworked the code for maintaining the notebook list
39
40
	def read(self):
41
		TextConfigFile.read(self)
42
		if len(self) > 0:
43
			if self[0] == '[NotebookList]\n':
44
				self.parse()
45
			else:
46
				self.parse_old_format()
47
48
	@staticmethod
49
	def _filter(line):
50
		return line and not line.isspace() and not line.startswith('#')
51
52
	def parse(self):
53
		'''Parses the notebook list format after reading it'''
54
		assert self.pop(0) == '[NotebookList]\n'
55
56
		# Parse key for default
57
		if self[0].startswith('Default='):
58
			k, v = self.pop(0).strip().split('=', 1)
59
			self.default = v
60
		else:
61
			self.default = None
62
63
		# Parse rest of list - assumed to be urls, but we check to be sure
64
		def map_to_uri(line):
65
			uri = line.strip()
66
			if not uri.startswith('file://'):
67
				uri = File(uri).uri
68
			return uri
69
70
		self[:] = map(map_to_uri, filter(self._filter, self))
71
72
	def parse_old_format(self):
73
		'''Method for backward compatibility'''
74
		# Old format is name, value pair, separated by whitespace
75
		# with all other whitespace escaped by a \
76
		# Default was _default_ which could refer a notebook name..
77
		import re
78
		fields_re = re.compile(r'(?:\\.|\S)+') # match escaped char or non-whitespace
79
		escaped_re = re.compile(r'\\(.)') # match single escaped char
80
		default = None
81
		locations = []
82
83
		lines = [line.strip() for line in filter(self._filter, self)]
84
		for line in lines:
85
			cols = fields_re.findall(line)
86
			if len(cols) == 2:
87
				name = escaped_re.sub(r'\1', cols[0])
88
				path = escaped_re.sub(r'\1', cols[1])
89
				if name == '_default_':
90
					default = path
91
				else:
92
					path = Dir(path).uri
93
					locations.append(path)
94
					if name == default:
95
						self.default = path
96
97
		if not self.default and default:
98
			self.default = Dir(default).uri
99
100
		self[:] = locations
101
102
	def write(self):
103
		lines = self[:] # copy
104
		lines.insert(0, '[NotebookList]')
105
		lines.insert(1, 'Default=%s' % (self.default or ''))
106
		lines = [line + '\n' for line in lines]
107
		self.file.writelines(lines)
108
109
	def get_names(self):
110
		'''Generator function that yield tuples with the notebook
111
		name and the notebook path.
112
		'''
113
		for path in self:
114
			name = self.get_name(path)
115
			if name:
116
				yield (name, path)
117
118
	def get_name(self, uri):
119
		# TODO support for paths that turn out to be files
120
		file = Dir(uri).file('notebook.zim')
121
		if file.exists():
122
			config = ConfigDictFile(file)
123
			if 'name' in config['Notebook']:
124
				return config['Notebook']['name']
125
		return None
126
127
	def get_by_name(self, name):
128
		for n, path in self.get_names():
154.1.3 by Jaap Karssenberg
Added daemon interaction for GtkInterface
129
			if n.lower() == name.lower():
154.1.2 by Jaap Karssenberg
Reworked the code for maintaining the notebook list
130
				return path
131
		else:
132
			return None
133
8 by Jaap Karssenberg
- Integrated templates with export and www code
134
135
85 by Jaap Karssenberg
Added support for INI-style config files
136
def get_notebook_list():
105 by Jaap Karssenberg
Various clean ups
137
	'''Returns a list of known notebooks'''
154.1.2 by Jaap Karssenberg
Reworked the code for maintaining the notebook list
138
	# TODO use weakref here
139
	return config_file('notebooks.list', klass=NotebookList)
140
141
142
def resolve_notebook(string):
143
	'''Takes either a notebook name or a file or dir path. For a name
144
	it resolves the path by looking for a notebook of that name in the
145
	notebook list. For a path it checks if this path points to a
146
	notebook or to a file in a notebook.
147
148
	It returns two values, a path to the notebook directory and an
149
	optional page path for a file inside a notebook. If the notebook
150
	was not found both values are None.
151
	'''
152
	assert isinstance(string, basestring)
153
175 by Jaap Karssenberg
* Added interwiki support
154
	page = None
155
	if is_url_re.match(string):
156
		assert string.startswith('file://')
157
		if '?' in string:
158
			filepath, page = string.split('?', 1)
159
		else:
154.1.2 by Jaap Karssenberg
Reworked the code for maintaining the notebook list
160
			filepath = string
175 by Jaap Karssenberg
* Added interwiki support
161
	elif os.path.sep in string:
162
		filepath = string
163
	else:
164
		nblist = get_notebook_list()
165
		filepath = nblist.get_by_name(string)
166
		if filepath is None:
154.1.2 by Jaap Karssenberg
Reworked the code for maintaining the notebook list
167
			return None, None # not found
168
169
	file = File(filepath) # Fixme need generic FS Path object here
170
	if filepath.endswith('notebook.zim'):
175 by Jaap Karssenberg
* Added interwiki support
171
		return File(filepath).dir, page
154.1.2 by Jaap Karssenberg
Reworked the code for maintaining the notebook list
172
	elif file.exists(): # file exists and really is a file
173
		parents = list(file)
174
		parents.reverse()
175
		for parent in parents:
176
			if File((parent, 'notebook.zim')).exists():
177
				page = file.relpath(parent)
178
				if '.' in page:
179
					page, _ = page.rsplit('.', 1) # remove extension
180
				page = Path(page.replace('/', ':'))
181
				return Dir(parent), page
182
		else:
183
			return None, None
184
	else:
175 by Jaap Karssenberg
* Added interwiki support
185
		return Dir(file.path), page
154.1.2 by Jaap Karssenberg
Reworked the code for maintaining the notebook list
186
187
	return notebook, path
188
189
190
def resolve_default_notebook():
191
	'''Returns a File or Dir object for the default notebook,
192
	or for the only notebook if there is only a single notebook
193
	in the list.
194
	'''
195
	default = None
196
	list = get_notebook_list()
197
	if list.default:
198
		default = list.default
199
	elif len(list) == 1:
200
		default = list[0]
201
202
	if default:
203
		if os.path.isfile(default):
204
			return File(default)
205
		else:
206
			return Dir(default)
207
	else:
208
		return None
209
210
211
def get_notebook(path):
212
	'''Convenience method that constructs a notebook from either a
213
	File or a Dir object.
214
	'''
215
	# TODO this is where the hook goes to automount etc.
216
	assert isinstance(path, (File, Dir))
217
	if path.exists():
218
		if isinstance(path, File):
219
			return Notebook(file=path)
220
		else:
221
			return Notebook(dir=path)
222
	else:
223
		return None
224
225
154.1.3 by Jaap Karssenberg
Added daemon interaction for GtkInterface
226
def get_default_notebook():
227
	'''Returns a Notebook object for the default notebook or None'''
228
	path = resolve_default_notebook()
229
	if path:
230
		return get_notebook(path)
231
	else:
232
		return None
233
234
154.1.2 by Jaap Karssenberg
Reworked the code for maintaining the notebook list
235
def init_notebook(path, name=None):
236
	'''Initialize a new notebook in a directory'''
237
	assert isinstance(path, Dir)
238
	path.touch()
239
	config = ConfigDictFile(path.file('notebook.zim'))
240
	config['Notebook']['name'] = name or path.basename
241
	# TODO auto detect if we should enable the slow_fs option
242
	config.write()
30 by Jaap Karssenberg
* Semi-functional notebookdialog
243
244
175 by Jaap Karssenberg
* Added interwiki support
245
def interwiki_link(link):
246
	'''Convert an interwiki link into an url'''
247
	assert isinstance(link, basestring) and '?' in link
248
	key, page = link.split('?', 1)
249
	url = None
250
	for line in config_file('urls.list'):
251
		if line.startswith(key+' ') or line.startswith(key+'\t'):
252
			url = line[len(key):].strip()
253
			break
254
	else:
255
		list = get_notebook_list()
256
		for name, path in list.get_names():
257
			if name.lower() == key.lower():
258
				url = path + '?{NAME}'
259
				break
260
261
	if url and is_url_re.match(url):
262
		if not ('{NAME}' in url or '{URL}' in url):
263
			url += '{URL}'
264
265
		url = url.replace('{NAME}', page)
266
		url = url.replace('{URL}', url_encode(page))
267
268
		return url
269
	else:
270
		return None
271
112 by Jaap Karssenberg
Moved Dialog to zim.gui.widgets to untangle recursive imports
272
class PageNameError(Error):
273
274
	description = _('''\
275
The given page name is not valid.
276
''') # T: error description
277
	# TODO add to explanation what are validcharacters
278
279
	def __init__(self, name):
280
		self.msg = _('Invalid page name "%s"') % name # T: error message
281
118 by Jaap Karssenberg
various fixes
282
112 by Jaap Karssenberg
Moved Dialog to zim.gui.widgets to untangle recursive imports
283
class LookupError(Error):
284
285
	description = '''\
286
Failed to lookup this page in the notebook storage.
287
This is likely a glitch in the application.
288
'''
289
174 by Jaap Karssenberg
* Fixed memory-leak in page index
290
class IndexBusyError(Error):
291
292
	description = '''\
293
Index is still busy updating while we try to do an
294
operation that needs the index.
295
'''
296
118 by Jaap Karssenberg
various fixes
297
112 by Jaap Karssenberg
Moved Dialog to zim.gui.widgets to untangle recursive imports
298
class PageExistsError(Error):
151 by Jaap Karssenberg
Drag-n-Drop in the treeview now works
299
	pass
300
301
	# TODO verbose description
12 by Jaap Karssenberg
Added code for resolving page names
302
118 by Jaap Karssenberg
various fixes
303
121.1.2 by Jaap Karssenberg
Implemented read-only state
304
class PageReadOnlyError(Error):
305
306
	# TODO verbose description
307
308
	def __init__(self, page):
309
		self.msg = _('Can not modify page: %s') % page.name
310
			# T: error message for read-only pages
311
89 by pardus
Basic move & delete page implemented
312
class Notebook(gobject.GObject):
105 by Jaap Karssenberg
Various clean ups
313
	'''Main class to access a notebook. Proxies between backend Store
314
	and Index objects on the one hand and the gui application on the other
162 by Jaap Karssenberg
Various fixes and manual updates
315
316
	This class has the following signals:
317
		* store-page (page)
318
		* move-page (oldpath, newpath, update_links)
319
		* delete-page (path)
320
		* properties-changed ()
321
322
	All signals are defined with the SIGNAL_RUN_LAST type, so any
323
	handler connected normally will run before the actual action.
324
	Use "connect_after()" to install handlers after storing, moving
325
	or deleting a page.
105 by Jaap Karssenberg
Various clean ups
326
	'''
3 by Jaap Karssenberg
sync
327
121.1.2 by Jaap Karssenberg
Implemented read-only state
328
	# TODO add checks for read-only page in much more methods
329
89 by pardus
Basic move & delete page implemented
330
	# define signals we want to use - (closure type, return type and arg types)
331
	__gsignals__ = {
162 by Jaap Karssenberg
Various fixes and manual updates
332
		'store-page': (gobject.SIGNAL_RUN_LAST, None, (object,)),
333
		'move-page': (gobject.SIGNAL_RUN_LAST, None, (object, object, bool)),
334
		'delete-page': (gobject.SIGNAL_RUN_LAST, None, (object,)),
120 by Jaap Karssenberg
Added properties dialog
335
		'properties-changed': (gobject.SIGNAL_RUN_FIRST, None, ()),
89 by pardus
Basic move & delete page implemented
336
	}
337
120 by Jaap Karssenberg
Added properties dialog
338
	properties = (
339
		('name', 'string', _('Name')), # T: label for properties dialog
340
		('home', 'page', _('Home Page')), # T: label for properties dialog
341
		('icon', 'image', _('Icon')), # T: label for properties dialog
342
		('document_root', 'dir', _('Document Root')), # T: label for properties dialog
343
		('slow_fs', 'bool', _('Slow file system')), # T: label for properties dialog
344
		#~ ('autosave', 'bool', _('Auto-version when closing the notebook')),
345
			# T: label for properties dialog
346
	)
347
348
	def __init__(self, dir=None, file=None, config=None, index=None):
349
		assert not (dir and file), 'BUG: can not provide both dir and file '
89 by pardus
Basic move & delete page implemented
350
		gobject.GObject.__init__(self)
59 by Jaap Karssenberg
Refactored notebook API around db index - tests OK, GUI still broken
351
		self._namespaces = []	# list used to resolve stores
352
		self._stores = {}		# dict mapping namespaces to stores
121.1.6 by Jaap Karssenberg
Added special template for Calendar pages
353
		self.namespace_properties = HierarchicDict()
59 by Jaap Karssenberg
Refactored notebook API around db index - tests OK, GUI still broken
354
		self._page_cache = weakref.WeakValueDictionary()
18 by Jaap Karssenberg
Added preliminary code to reslve files
355
		self.dir = None
120 by Jaap Karssenberg
Added properties dialog
356
		self.file = None
62 by Jaap Karssenberg
index is now actually saved
357
		self.cache_dir = None
120 by Jaap Karssenberg
Added properties dialog
358
		self.name = None
359
		self.icon = None
112 by Jaap Karssenberg
Moved Dialog to zim.gui.widgets to untangle recursive imports
360
		self.config = config
4 by Jaap Karssenberg
Added classes Notebook, Page and PageList
361
120 by Jaap Karssenberg
Added properties dialog
362
		if dir:
363
			assert isinstance(dir, Dir)
364
			self.dir = dir
121.1.2 by Jaap Karssenberg
Implemented read-only state
365
			self.readonly = not dir.iswritable()
141 by Jaap Karssenberg
various bug fixes
366
			self.cache_dir = dir.subdir('.zim')
367
			if self.readonly or not self.cache_dir.iswritable():
121.1.2 by Jaap Karssenberg
Implemented read-only state
368
				self.cache_dir = self._cache_dir(dir)
62 by Jaap Karssenberg
index is now actually saved
369
			logger.debug('Cache dir: %s', self.cache_dir)
112 by Jaap Karssenberg
Moved Dialog to zim.gui.widgets to untangle recursive imports
370
			if self.config is None:
120 by Jaap Karssenberg
Added properties dialog
371
				self.config = ConfigDictFile(dir.file('notebook.zim'))
12 by Jaap Karssenberg
Added code for resolving page names
372
			# TODO check if config defined root namespace
60 by Jaap Karssenberg
PageTreeStore is now working correctly
373
			self.add_store(Path(':'), 'files') # set root
12 by Jaap Karssenberg
Added code for resolving page names
374
			# TODO add other namespaces from config
120 by Jaap Karssenberg
Added properties dialog
375
		elif file:
376
			assert isinstance(file, File)
377
			self.file = file
121.1.2 by Jaap Karssenberg
Implemented read-only state
378
			self.readonly = not file.iswritable()
12 by Jaap Karssenberg
Added code for resolving page names
379
			assert False, 'TODO: support for single file notebooks'
4 by Jaap Karssenberg
Added classes Notebook, Page and PageList
380
62 by Jaap Karssenberg
index is now actually saved
381
		if index is None:
382
			import zim.index # circular import
383
			self.index = zim.index.Index(notebook=self)
384
		else:
385
			self.index = index
386
			self.index.set_notebook(self)
387
112 by Jaap Karssenberg
Moved Dialog to zim.gui.widgets to untangle recursive imports
388
		if self.config is None:
389
			self.config = ConfigDict()
120 by Jaap Karssenberg
Added properties dialog
390
121 by Jaap Karssenberg
Updated Export dialog options
391
		self.config['Notebook'].setdefault('name', None, klass=basestring)
392
		self.config['Notebook'].setdefault('home', ':Home', klass=basestring)
393
		self.config['Notebook'].setdefault('icon', None, klass=basestring)
394
		self.config['Notebook'].setdefault('document_root', None, klass=basestring)
120 by Jaap Karssenberg
Added properties dialog
395
		self.config['Notebook'].setdefault('slow_fs', False)
396
		self.do_properties_changed()
397
141 by Jaap Karssenberg
various bug fixes
398
	@property
399
	def uri(self):
400
		'''Returns a file:// uri for this notebook that can be opened by zim'''
401
		assert self.dir or self.file, 'Notebook does not have a dir or file'
402
		if self.dir:
403
			return self.dir.uri
404
		else:
405
			return self.file.uri
406
121.1.2 by Jaap Karssenberg
Implemented read-only state
407
	def _cache_dir(self, dir):
408
		from zim.config import XDG_CACHE_HOME
409
		path = 'notebook-' + dir.path.replace('/', '_').strip('_')
410
		return XDG_CACHE_HOME.subdir(('zim', path))
411
120 by Jaap Karssenberg
Added properties dialog
412
	def save_properties(self, **properties):
413
		# Check if icon is relative
179 by Jaap Karssenberg
Two last minute fixes
414
		if 'icon' in properties and properties['icon'] \
415
		and self.dir and properties['icon'].startswith(self.dir.path):
120 by Jaap Karssenberg
Added properties dialog
416
			i = len(self.dir.path)
417
			path = './' + properties['icon'][i:].lstrip('/\\')
418
			# TODO use proper fs routine(s) for this substitution
419
			properties['icon'] = path
420
179 by Jaap Karssenberg
Two last minute fixes
421
		# Set home page as string
422
		if 'home' in properties and isinstance(properties['home'], Path):
423
			properties['home'] = properties['home'].name
424
120 by Jaap Karssenberg
Added properties dialog
425
		self.config['Notebook'].update(properties)
426
		self.config.write()
427
		self.emit('properties-changed')
428
429
	def do_properties_changed(self):
430
		#~ import pprint
431
		#~ pprint.pprint(self.config)
432
		config = self.config['Notebook']
433
434
		# Set a name for ourselves
435
		if config['name']: 	self.name = config['name']
436
		elif self.dir: self.name = self.dir.basename
437
		elif self.file: self.name = self.file.basename
438
		else: self.name = 'Unnamed Notebook'
439
440
		# We should always have a home
441
		config.setdefault('home', ':Home')
442
443
		# Resolve icon, can be relative
444
		# TODO proper FS routine to check abs path - also allowed without the "./" - so e.g. icon.png should be resolved as well
445
		if self.dir and config['icon'] and config['icon'].startswith('.'):
446
			self.icon = self.dir.file(config['icon']).path
447
		elif config['icon']:
448
			self.icon = File(config['icon']).path
449
		else:
450
			self.icon = None
451
452
		# Set FS property
453
		if config['slow_fs']: print 'TODO: hook slow_fs property'
62 by Jaap Karssenberg
index is now actually saved
454
59 by Jaap Karssenberg
Refactored notebook API around db index - tests OK, GUI still broken
455
	def add_store(self, path, store, **args):
456
		'''Add a store to the notebook to handle a specific path and all
457
		it's sub-pages. Needs a Path and a store name, all other args will
116 by Jaap Karssenberg
Moved test data to XML file to avoid issues with utf-8 filenames after unzip
458
		be passed to the store. Alternatively you can pass a store object
459
		but in that case no arguments are allowed.
460
		Returns the store object.
4 by Jaap Karssenberg
Added classes Notebook, Page and PageList
461
		'''
59 by Jaap Karssenberg
Refactored notebook API around db index - tests OK, GUI still broken
462
		assert not path.name in self._stores, 'Store for "%s" exists' % path
116 by Jaap Karssenberg
Moved test data to XML file to avoid issues with utf-8 filenames after unzip
463
		if isinstance(store, basestring):
464
			mod = zim.stores.get_store(store)
465
			mystore = mod.Store(notebook=self, path=path, **args)
466
		else:
467
			assert not args
468
			mystore = store
59 by Jaap Karssenberg
Refactored notebook API around db index - tests OK, GUI still broken
469
		self._stores[path.name] = mystore
470
		self._namespaces.append(path.name)
4 by Jaap Karssenberg
Added classes Notebook, Page and PageList
471
472
		# keep order correct for lookup
59 by Jaap Karssenberg
Refactored notebook API around db index - tests OK, GUI still broken
473
		self._namespaces.sort(reverse=True)
4 by Jaap Karssenberg
Added classes Notebook, Page and PageList
474
12 by Jaap Karssenberg
Added code for resolving page names
475
		return mystore
476
59 by Jaap Karssenberg
Refactored notebook API around db index - tests OK, GUI still broken
477
	def get_store(self, path):
12 by Jaap Karssenberg
Added code for resolving page names
478
		'''Returns the store object to handle a page or namespace.'''
59 by Jaap Karssenberg
Refactored notebook API around db index - tests OK, GUI still broken
479
		for namespace in self._namespaces:
480
			# longest match first because of reverse sorting
481
			if namespace == ''			\
482
			or page.name == namespace	\
483
			or page.name.startswith(namespace+':'):
484
				return self._stores[namespace]
485
		else:
486
			raise LookupError, 'Could not find store for: %s' % name
3 by Jaap Karssenberg
sync
487
121.1.7 by Jaap Karssenberg
Added search dialog
488
	def get_stores(self):
489
		return self._stores.values()
490
121.1.8 by Jaap Karssenberg
Implemented updatign links on move
491
	def resolve_path(self, name, source=None, index=None):
59 by Jaap Karssenberg
Refactored notebook API around db index - tests OK, GUI still broken
492
		'''Returns a proper path name for page names given in links
121.1.8 by Jaap Karssenberg
Implemented updatign links on move
493
		or from user input. The optional argument 'source' is the
494
		path for the refering page, if any, or the path of the "current"
495
		page in the user interface.
59 by Jaap Karssenberg
Refactored notebook API around db index - tests OK, GUI still broken
496
66 by pardus
LinkMap now understands the index :)
497
		The 'index' argument allows specifying an index object, if
498
		none is given the default index for this notebook is used.
499
121.1.8 by Jaap Karssenberg
Implemented updatign links on move
500
		If no source path is given or if the page name starts with
59 by Jaap Karssenberg
Refactored notebook API around db index - tests OK, GUI still broken
501
		a ':' the name is considered an absolute name and only case is
12 by Jaap Karssenberg
Added code for resolving page names
502
		resolved. If the page does not exist the last part(s) of the
503
		name will remain in the case as given.
504
121.1.8 by Jaap Karssenberg
Implemented updatign links on move
505
		If a source path is given and the page name starts with '+'
506
		it will be resolved as a direct child of the source.
507
508
		Else we first look for a match of the first part of the name in the
509
		source path. If that fails we do a search for the first part of
510
		the name through all namespaces in the source path, starting with
511
		pages below the namespace of the source. If no existing page was
512
		found in this search we default to a new page below this namespace.
513
514
		So if we for example look for "baz" with as source ":foo:bar:dus"
59 by Jaap Karssenberg
Refactored notebook API around db index - tests OK, GUI still broken
515
		the following pages will be checked in a case insensitive way:
516
517
			:foo:bar:baz
518
			:foo:baz
519
			:baz
520
521
		And if none exist we default to ":foo:bar:baz"
522
121.1.8 by Jaap Karssenberg
Implemented updatign links on move
523
		However if for example we are looking for "bar:bud" with as source
524
		":foo:bar:baz:dus", we only try to resolve the case for ":foo:bar:bud"
59 by Jaap Karssenberg
Refactored notebook API around db index - tests OK, GUI still broken
525
		and default to the given case if it does not yet exist.
526
527
		This method will raise a PageNameError if the name resolves
528
		to an empty string. Since all trailing ":" characters are removed
529
		there is no way for the name to address the root path in this method -
530
		and typically user input should not need to able to address this path.
12 by Jaap Karssenberg
Added code for resolving page names
531
		'''
121.1.8 by Jaap Karssenberg
Implemented updatign links on move
532
		assert name, 'BUG: name is empty string'
533
		startswith = name[0]
121.2.1 by Jaap Karssenberg
Added ParseTreeBuilder to reformat oageview dump into nicer tree
534
		if startswith == '.':
535
			startswith = '+' # backward compat
121.1.8 by Jaap Karssenberg
Implemented updatign links on move
536
		if startswith == '+':
537
			name = name[1:]
89 by pardus
Basic move & delete page implemented
538
		name = self.cleanup_pathname(name)
12 by Jaap Karssenberg
Added code for resolving page names
539
66 by pardus
LinkMap now understands the index :)
540
		if index is None:
541
			index = self.index
542
121.1.8 by Jaap Karssenberg
Implemented updatign links on move
543
		if startswith == ':' or source == None:
66 by pardus
LinkMap now understands the index :)
544
			return index.resolve_case(name) or Path(name)
121.1.8 by Jaap Karssenberg
Implemented updatign links on move
545
		elif startswith == '+':
177 by Jaap Karssenberg
* Fixed parsing utf8 commandline arguments
546
			if not source:
547
				raise PageNameError, '+'+name
121.1.8 by Jaap Karssenberg
Implemented updatign links on move
548
			return index.resolve_case(source.name+':'+name)  \
549
						or Path(source.name+':'+name)
550
			# FIXME use parent as argument
12 by Jaap Karssenberg
Added code for resolving page names
551
		else:
552
			# first check if we see an explicit match in the path
121.1.8 by Jaap Karssenberg
Implemented updatign links on move
553
			assert isinstance(source, Path)
50 by Jaap Karssenberg
Lot of GUI work
554
			anchor = name.split(':')[0].lower()
121.1.8 by Jaap Karssenberg
Implemented updatign links on move
555
			path = source.namespace.lower().split(':')
12 by Jaap Karssenberg
Added code for resolving page names
556
			if anchor in path:
557
				# ok, so we can shortcut to an absolute path
43 by Jaap Karssenberg
* Worked on tests
558
				path.reverse() # why is there no rindex or rfind ?
12 by Jaap Karssenberg
Added code for resolving page names
559
				i = path.index(anchor) + 1
43 by Jaap Karssenberg
* Worked on tests
560
				path = path[i:]
561
				path.reverse()
12 by Jaap Karssenberg
Added code for resolving page names
562
				path.append( name.lstrip(':') )
563
				name = ':'.join(path)
66 by pardus
LinkMap now understands the index :)
564
				return index.resolve_case(name) or Path(name)
121.1.8 by Jaap Karssenberg
Implemented updatign links on move
565
				# FIXME use parentt as argument
59 by Jaap Karssenberg
Refactored notebook API around db index - tests OK, GUI still broken
566
				# FIXME use short cut when the result is the parent
12 by Jaap Karssenberg
Added code for resolving page names
567
			else:
59 by Jaap Karssenberg
Refactored notebook API around db index - tests OK, GUI still broken
568
				# no luck, do a search through the whole path - including root
121.1.8 by Jaap Karssenberg
Implemented updatign links on move
569
				source = index.lookup_path(source) or source
570
				for parent in source.parents():
66 by pardus
LinkMap now understands the index :)
571
					candidate = index.resolve_case(name, namespace=parent)
59 by Jaap Karssenberg
Refactored notebook API around db index - tests OK, GUI still broken
572
					if not candidate is None:
573
						return candidate
574
				else:
575
					# name not found, keep case as is
121.1.8 by Jaap Karssenberg
Implemented updatign links on move
576
					return source.parent + name
577
578
	def relative_link(self, source, href):
579
		'''Returns a link for a path 'href' relative to path 'source'.
580
		More or less the opposite of resolve_path().
581
		'''
582
		if href == source:
583
			return href.basename
584
		elif href > source:
585
			return '+' + href.relname(source)
586
		else:
587
			parent = source.commonparent(href)
588
			if parent.isroot:
589
				return ':' + href.name
590
			else:
591
				return parent.basename + ':' + href.relname(parent)
59 by Jaap Karssenberg
Refactored notebook API around db index - tests OK, GUI still broken
592
156 by Jaap Karssenberg
* "Insert Date" dialog can now link to calendar pages
593
	def register_hook(self, name, handler):
594
		'''Register a handler method for a specific hook'''
595
		register = '_register_%s' % name
596
		if not hasattr(self, register):
597
			setattr(self, register, [])
598
		getattr(self, register).append(handler)
599
600
	def unregister_hook(self, name, handler):
601
		'''Remove a handler method for a specific hook'''
602
		register = '_register_%s' % name
603
		if hasattr(self, register):
604
			getattr(self, register).remove(handler)
605
606
	def suggest_link(self, source, word):
162 by Jaap Karssenberg
Various fixes and manual updates
607
		'''Suggest a link Path for 'word' or return None if no suggestion is
156 by Jaap Karssenberg
* "Insert Date" dialog can now link to calendar pages
608
		found. By default we do not do any suggestion but plugins can
609
		register handlers to add suggestions. See 'register_hook()' to
610
		register a handler.
611
		'''
612
		if not  hasattr(self, '_register_suggest_link'):
613
			return None
614
615
		for handler in self._register_suggest_link:
616
			link = handler(source, word)
617
			if not link is None:
618
				return link
619
		else:
620
			return None
621
116 by Jaap Karssenberg
Moved test data to XML file to avoid issues with utf-8 filenames after unzip
622
	@staticmethod
623
	def cleanup_pathname(name):
89 by pardus
Basic move & delete page implemented
624
		'''Returns a safe version of name, used internally by functions like
625
		resolve_path() to parse user input.
626
		'''
112 by Jaap Karssenberg
Moved Dialog to zim.gui.widgets to untangle recursive imports
627
		orig = name
185 by Jaap Karssenberg
* Fix that removes duplicate entries with '_' in index
628
		name = name.replace('_', ' ')
629
			# Avoid duplicates with and without '_' in index
89 by pardus
Basic move & delete page implemented
630
		name = ':'.join( map(unicode.strip,
631
				filter(lambda n: len(n)>0, unicode(name).split(':')) ) )
632
128 by Jaap Karssenberg
Small fixes
633
		# Reserved characters are:
634
		# The ':' is reserrved as seperator
635
		# The '?' is reserved to encode url style options
636
		# The '#' is reserved as anchor separator
637
		# The '/' and '\' are reserved to distinquise file links & urls
638
		# First character of each part MUST be alphanumeric
639
		#		(including utf8 letters / numbers)
640
641
		# Zim version < 0.42 restricted all special charachters but
642
		# white listed ".", "-", "_", "(", ")", ":" and "%".
643
89 by pardus
Basic move & delete page implemented
644
		# TODO check for illegal characters in the name
645
646
		if not name or name.isspace():
112 by Jaap Karssenberg
Moved Dialog to zim.gui.widgets to untangle recursive imports
647
			raise PageNameError, orig
89 by pardus
Basic move & delete page implemented
648
649
		return name
650
59 by Jaap Karssenberg
Refactored notebook API around db index - tests OK, GUI still broken
651
	def get_page(self, path):
97 by Jaap Karssenberg
Implemented logic for saving pages and related error handling
652
		'''Returns a Page object. This method uses a weakref dictionary to
653
		ensure that an unique object is being used for each page that is
654
		given out.
655
		'''
114 by Jaap Karssenberg
Fixes for the refactoring of Dialog to zim.gui.widgets
656
		# As a special case, using an invalid page as the argument should
657
		# return a valid page object.
59 by Jaap Karssenberg
Refactored notebook API around db index - tests OK, GUI still broken
658
		assert isinstance(path, Path)
114 by Jaap Karssenberg
Fixes for the refactoring of Dialog to zim.gui.widgets
659
		if path.name in self._page_cache \
660
		and self._page_cache[path.name].valid:
59 by Jaap Karssenberg
Refactored notebook API around db index - tests OK, GUI still broken
661
			return self._page_cache[path.name]
3 by Jaap Karssenberg
sync
662
		else:
59 by Jaap Karssenberg
Refactored notebook API around db index - tests OK, GUI still broken
663
			store = self.get_store(path)
664
			page = store.get_page(path)
665
			# TODO - set haschildren if page maps to a store namespace
666
			self._page_cache[path.name] = page
3 by Jaap Karssenberg
sync
667
			return page
668
97 by Jaap Karssenberg
Implemented logic for saving pages and related error handling
669
	def flush_page_cache(self, path):
670
		'''Remove a page from the page cache, calling get_page() after this
671
		will return a fresh page object. Be aware that the old object
672
		may still be around but will have its 'valid' attribute set to False.
121.1.8 by Jaap Karssenberg
Implemented updatign links on move
673
		This function also removes all child pages of path from the cache.
97 by Jaap Karssenberg
Implemented logic for saving pages and related error handling
674
		'''
121.1.8 by Jaap Karssenberg
Implemented updatign links on move
675
		names = [path.name]
676
		ns = path.name + ':'
677
		names.extend(k for k in self._page_cache.keys() if k.startswith(ns))
678
		for name in names:
679
			if name in self._page_cache:
680
				page = self._page_cache[name]
681
				assert not page.modified, 'BUG: Flushing page with unsaved changes'
682
				page.valid = False
683
				del self._page_cache[name]
97 by Jaap Karssenberg
Implemented logic for saving pages and related error handling
684
28 by Jaap Karssenberg
initial side pane tree index
685
	def get_home_page(self):
686
		'''Returns a page object for the home page.'''
112 by Jaap Karssenberg
Moved Dialog to zim.gui.widgets to untangle recursive imports
687
		path = self.resolve_path(self.config['Notebook']['home'])
688
		return self.get_page(path)
59 by Jaap Karssenberg
Refactored notebook API around db index - tests OK, GUI still broken
689
690
	def get_pagelist(self, path):
691
		'''Returns a list of page objects.'''
692
		store = self.get_store(path)
693
		return store.get_pagelist(path)
694
		# TODO: add sub-stores in this namespace if any
56 by Jaap Karssenberg
Structural improvements of the template module and related code
695
97 by Jaap Karssenberg
Implemented logic for saving pages and related error handling
696
	def store_page(self, page):
697
		'''Store a page permanently. Commits the parse tree from the page
698
		object to the backend store.
699
		'''
700
		assert page.valid, 'BUG: page object no longer valid'
162 by Jaap Karssenberg
Various fixes and manual updates
701
		with SignalExceptionContext(self, 'store-page'):
702
			self.emit('store-page', page)
703
704
	def do_store_page(self, page):
705
		with SignalRaiseExceptionContext(self, 'store-page'):
706
			store = self.get_store(page)
707
			store.store_page(page)
97 by Jaap Karssenberg
Implemented logic for saving pages and related error handling
708
709
	def revert_page(self, page):
710
		'''Reloads the parse tree from the store into the page object.
711
		In a sense the opposite to store_page(). Used in the gui to
712
		discard changes in a page.
713
		'''
714
		# get_page without the cache
715
		assert page.valid, 'BUG: page object no longer valid'
716
		store = self.get_store(page)
717
		storedpage = store.get_page(page)
718
		page.set_parsetree(storedpage.get_parsetree())
719
		page.modified = False
720
89 by pardus
Basic move & delete page implemented
721
	def move_page(self, path, newpath, update_links=True):
105 by Jaap Karssenberg
Various clean ups
722
		'''Move a page from 'path' to 'newpath'. If 'update_links' is
723
		True all links from and to the page will be modified as well.
724
		'''
174 by Jaap Karssenberg
* Fixed memory-leak in page index
725
		if update_links and self.index.updating:
726
			raise IndexBusyError, 'Index busy'
727
			# Index need to be complete in order to be 100% sure we
728
			# know all backlinks, so no way we can update links before.
729
121.1.8 by Jaap Karssenberg
Implemented updatign links on move
730
		page = self.get_page(path)
731
		if not (page.hascontent or page.haschildren):
732
			raise LookupError, 'Page does not exist: %s' % path.name
733
		assert not page.modified, 'BUG: moving a page with uncomitted changes'
734
162 by Jaap Karssenberg
Various fixes and manual updates
735
		with SignalExceptionContext(self, 'move-page'):
736
			self.emit('move-page', path, newpath, update_links)
737
738
	def do_move_page(self, path, newpath, update_links):
739
		logger.debug('Move %s to %s (%s)', path, newpath, update_links)
740
741
		with SignalRaiseExceptionContext(self, 'move-page'):
742
			# Collect backlinks
743
			if update_links:
744
				from zim.index import LINK_DIR_BACKWARD
745
				backlinkpages = set(
121.1.8 by Jaap Karssenberg
Implemented updatign links on move
746
					l.source for l in
162 by Jaap Karssenberg
Various fixes and manual updates
747
						self.index.list_links(path, LINK_DIR_BACKWARD) )
748
				for child in self.index.walk(path):
749
					backlinkpages.update(set(
750
						l.source for l in
751
							self.index.list_links(path, LINK_DIR_BACKWARD) ))
752
753
			# Do the actual move
754
			store = self.get_store(path)
755
			newstore = self.get_store(newpath)
756
			if newstore == store:
757
				store.move_page(path, newpath)
758
			else:
759
				assert False, 'TODO: move between stores'
760
				# recursive + move attachments as well
761
762
			self.flush_page_cache(path)
763
			self.flush_page_cache(newpath)
764
765
			# Update links in moved pages
766
			page = self.get_page(newpath)
767
			if page.hascontent:
768
				self._update_links_from(page, path)
769
				store = self.get_store(page)
770
				store.store_page(page)
771
				# do not use self.store_page because it emits signals
772
			for child in self._no_index_walk(newpath):
773
				if not child.hascontent:
121.1.8 by Jaap Karssenberg
Implemented updatign links on move
774
					continue
162 by Jaap Karssenberg
Various fixes and manual updates
775
				oldpath = path + child.relname(newpath)
776
				self._update_links_from(child, oldpath)
777
				store = self.get_store(child)
778
				store.store_page(child)
779
				# do not use self.store_page because it emits signals
780
781
			# Update links to the moved page tree
782
			if update_links:
783
				# Need this indexed before we can resolve links to it
784
				self.index.delete(path)
785
				self.index.update(newpath)
786
				#~ print backlinkpages
787
				for p in backlinkpages:
788
					if p == path or p > path:
789
						continue
790
					page = self.get_page(p)
791
					self._update_links_in_page(page, path, newpath)
792
					self.store_page(page)
121.1.8 by Jaap Karssenberg
Implemented updatign links on move
793
794
	def _no_index_walk(self, path):
795
		'''Walking that can be used when the index is not in sync'''
796
		# TODO allow this to cross several stores
797
		store = self.get_store(path)
798
		for page in store.get_pagelist(path):
799
			yield page
800
			for child in self._no_index_walk(page): # recurs
801
				yield child
802
803
	@staticmethod
804
	def _update_link_tag(tag, newhref):
176 by Jaap Karssenberg
* Improved page update on delete & move
805
		newhref = str(newhref)
121.1.8 by Jaap Karssenberg
Implemented updatign links on move
806
		haschildren = bool(list(tag.getchildren()))
807
		if not haschildren and tag.text == tag.attrib['href']:
808
			tag.text = newhref
176 by Jaap Karssenberg
* Improved page update on delete & move
809
		tag.attrib['href'] = newhref
121.1.8 by Jaap Karssenberg
Implemented updatign links on move
810
811
	def _update_links_from(self, page, oldpath):
812
		logger.debug('Updating links in %s (was %s)', page, oldpath)
813
		tree = page.get_parsetree()
814
		if not tree:
815
			return
816
817
		for tag in tree.getiterator('link'):
818
			href = tag.attrib['href']
819
			type = link_type(href)
820
			if type == 'page':
821
				hrefpath = self.resolve_path(href, source=page)
822
				oldhrefpath = self.resolve_path(href, source=oldpath)
823
				#~ print 'LINK', oldhrefpath, '->', hrefpath
824
				if hrefpath != oldhrefpath:
825
					if hrefpath >= page and oldhrefpath >= oldpath:
826
						#~ print '\t.. Ignore'
827
						pass
828
					else:
829
						newhref = self.relative_link(page, oldhrefpath)
830
						#~ print '\t->', newhref
831
						self._update_link_tag(tag, newhref)
832
833
		page.set_parsetree(tree)
834
835
	def _update_links_in_page(self, page, oldpath, newpath):
836
		# Maybe counter intuitive, but pages below oldpath do not need
837
		# to exist anymore while we still try to resolve links to these
838
		# pages. The reason is that all pages that could link _upward_
839
		# to these pages are below and are moved as well.
840
		logger.debug('Updating links in %s to %s (was: %s)', page, newpath, oldpath)
841
		tree = page.get_parsetree()
842
		if not tree:
843
			logger.warn('Page turned out to be empty: %s', page)
844
			return
845
846
		for tag in tree.getiterator('link'):
847
			href = tag.attrib['href']
848
			type = link_type(href)
849
			if type == 'page':
850
				hrefpath = self.resolve_path(href, source=page)
851
				#~ print 'LINK', hrefpath
852
				if hrefpath == oldpath:
853
					newhrefpath = newpath
854
					#~ print '\t==', oldpath, '->', newhrefpath
855
				elif hrefpath > oldpath:
856
					rel = hrefpath.relname(oldpath)
857
					newhrefpath = newpath + rel
858
					#~ print '\t>', oldpath, '->', newhrefpath
859
				else:
860
					continue
861
862
				newhref = self.relative_link(page, newhrefpath)
863
				self._update_link_tag(tag, newhref)
864
865
		page.set_parsetree(tree)
89 by pardus
Basic move & delete page implemented
866
867
	def rename_page(self, path, newbasename,
868
						update_heading=True, update_links=True):
105 by Jaap Karssenberg
Various clean ups
869
		'''Rename page to a page in the same namespace but with a new basename.
111.1.1 by Jaap Karssenberg
Added rough calendar plugin
870
		If 'update_heading' is True the first heading in the page will be updated to it's
105 by Jaap Karssenberg
Various clean ups
871
		new name.  If 'update_links' is True all links from and to the page will be
872
		modified as well.
873
		'''
89 by pardus
Basic move & delete page implemented
874
		logger.debug('Rename %s to "%s" (%s, %s)',
875
			path, newbasename, update_heading, update_links)
876
877
		newbasename = self.cleanup_pathname(newbasename)
878
		newpath = Path(path.namespace + ':' + newbasename)
879
		if newbasename.lower() != path.basename.lower():
880
			# allow explicit case-sensitive renaming
881
			newpath = self.index.resolve_case(
116 by Jaap Karssenberg
Moved test data to XML file to avoid issues with utf-8 filenames after unzip
882
				newbasename, namespace=path.parent) or newpath
89 by pardus
Basic move & delete page implemented
883
884
		self.move_page(path, newpath, update_links=update_links)
885
		if update_heading:
886
			page = self.get_page(newpath)
887
			tree = page.get_parsetree()
152 by Jaap Karssenberg
Pasting zim content as Html now works both on linux and windows
888
			if not tree is None:
889
				tree.set_heading(newbasename.title())
890
				page.set_parsetree(tree)
891
				self.store_page(page)
89 by pardus
Basic move & delete page implemented
892
893
		return newpath
894
895
	def delete_page(self, path):
162 by Jaap Karssenberg
Various fixes and manual updates
896
		with SignalExceptionContext(self, 'delete-page'):
897
			self.emit('delete-page', path)
898
899
	def do_delete_page(self, path):
900
		with SignalRaiseExceptionContext(self, 'delete-page'):
901
			store = self.get_store(path)
902
			store.delete_page(path)
903
			self.flush_page_cache(path)
12 by Jaap Karssenberg
Added code for resolving page names
904
94 by Jaap Karssenberg
progress on insert and edit dialogs
905
	def resolve_file(self, filename, path):
71.1.3 by pardus
Some support for images
906
		'''Resolves a file or directory path relative to a page. Returns a
907
		File object. However the file does not have to exist.
43 by Jaap Karssenberg
* Worked on tests
908
909
		File urls and paths that start with '~/' or '~user/' are considered
910
		absolute paths and are returned unmodified.
911
912
		In case the file path starts with '/' the the path is taken relative
96 by Jaap Karssenberg
updated exportdialog and introduced Linker objects
913
		to the document root - this can e.g. be a parent directory of the
914
		notebook. Defaults to the home dir.
43 by Jaap Karssenberg
* Worked on tests
915
916
		Other paths are considered attachments and are resolved relative
71.1.3 by pardus
Some support for images
917
		to the namespce below the page.
135 by Jaap Karssenberg
* Headings and indent will now always claim the whole line
918
919
		Because this is used to resolve file links and is supposed to be
920
		platform independent it tries to convert windows filenames to
921
		unix equivalents.
43 by Jaap Karssenberg
* Worked on tests
922
		'''
135 by Jaap Karssenberg
* Headings and indent will now always claim the whole line
923
		filename = filename.replace('\\', '/')
94 by Jaap Karssenberg
progress on insert and edit dialogs
924
		if filename.startswith('~') or filename.startswith('file:/'):
925
			return File(filename)
926
		elif filename.startswith('/'):
96 by Jaap Karssenberg
updated exportdialog and introduced Linker objects
927
			dir = self.get_document_root() or Dir('~')
94 by Jaap Karssenberg
progress on insert and edit dialogs
928
			return dir.file(filename)
135 by Jaap Karssenberg
* Headings and indent will now always claim the whole line
929
		elif is_win32_path_re.match(filename):
930
			if not filename.startswith('/'):
931
				filename = '/'+filename
932
				# make absolute on unix
933
			return File(filename)
43 by Jaap Karssenberg
* Worked on tests
934
		else:
71.1.3 by pardus
Some support for images
935
			# TODO - how to deal with '..' in the middle of the path ?
94 by Jaap Karssenberg
progress on insert and edit dialogs
936
			filepath = [p for p in filename.split('/') if len(p) and p != '.']
96 by Jaap Karssenberg
updated exportdialog and introduced Linker objects
937
			if not filepath: # filename is e.g. "."
938
				return self.get_attachments_dir(path)
71.1.3 by pardus
Some support for images
939
			pagepath = path.name.split(':')
43 by Jaap Karssenberg
* Worked on tests
940
			filename = filepath.pop()
71.1.3 by pardus
Some support for images
941
			while filepath and filepath[0] == '..':
942
				if not pagepath:
943
					print 'TODO: handle paths relative to notebook but outside notebook dir'
944
					return File('/TODO')
43 by Jaap Karssenberg
* Worked on tests
945
				else:
71.1.3 by pardus
Some support for images
946
					filepath.pop(0)
947
					pagepath.pop()
948
			pagename = ':'+':'.join(pagepath + filepath)
86 by Jaap Karssenberg
* Added documents folder and attachments folder actions
949
			dir = self.get_attachments_dir(Path(pagename))
43 by Jaap Karssenberg
* Worked on tests
950
			return dir.file(filename)
4 by Jaap Karssenberg
Added classes Notebook, Page and PageList
951
94 by Jaap Karssenberg
progress on insert and edit dialogs
952
	def relative_filepath(self, file, path=None):
953
		'''Returns a filepath relative to either the documents dir (/xxx), the
95 by Jaap Karssenberg
Fixed new page, delete page & attach file dialogs
954
		attachments dir (if a path is given) (./xxx or ../xxx) or the users
94 by Jaap Karssenberg
progress on insert and edit dialogs
955
		home dir (~/xxx). Returns None otherwise.
956
957
		Intended as the counter part of resolve_file().
958
		Typically this function is used to present the user with readable paths
959
		or to shorten the paths inserted in the wiki code. It is advised to
95 by Jaap Karssenberg
Fixed new page, delete page & attach file dialogs
960
		use file uris for links that can not be made relative.
94 by Jaap Karssenberg
progress on insert and edit dialogs
961
		'''
962
		if path:
963
			root = self.dir
964
			dir = self.get_attachments_dir(path)
965
			if file.ischild(dir):
121.1.11 by Jaap Karssenberg
Fixes based on testing python2.5 on win32
966
				return './'+file.relpath(dir)
94 by Jaap Karssenberg
progress on insert and edit dialogs
967
			elif root and file.ischild(root) and dir.ischild(root):
121.1.11 by Jaap Karssenberg
Fixes based on testing python2.5 on win32
968
				parent = file.commonparent(dir)
969
				uppath = dir.relpath(parent)
970
				downpath = file.relpath(parent)
971
				up = 1 + uppath.count('/')
972
				return '../'*up + downpath
94 by Jaap Karssenberg
progress on insert and edit dialogs
973
96 by Jaap Karssenberg
updated exportdialog and introduced Linker objects
974
		dir = self.get_document_root()
975
		if dir and file.ischild(dir):
121.1.11 by Jaap Karssenberg
Fixes based on testing python2.5 on win32
976
			return '/'+file.relpath(dir)
94 by Jaap Karssenberg
progress on insert and edit dialogs
977
96 by Jaap Karssenberg
updated exportdialog and introduced Linker objects
978
		dir = Dir('~')
94 by Jaap Karssenberg
progress on insert and edit dialogs
979
		if file.ischild(dir):
121.1.11 by Jaap Karssenberg
Fixes based on testing python2.5 on win32
980
			return '~/'+file.relpath(dir)
94 by Jaap Karssenberg
progress on insert and edit dialogs
981
982
		return None
983
86 by Jaap Karssenberg
* Added documents folder and attachments folder actions
984
	def get_attachments_dir(self, path):
985
		'''Returns a Dir object for the attachments directory for 'path'.
986
		The directory does not need to exist.
987
		'''
988
		store = self.get_store(path)
989
		return store.get_attachments_dir(path)
990
96 by Jaap Karssenberg
updated exportdialog and introduced Linker objects
991
	def get_document_root(self):
992
		'''Returns the Dir object for the document root or None'''
120 by Jaap Karssenberg
Added properties dialog
993
		path = self.config['Notebook']['document_root']
994
		if path: return Dir(path)
995
		else: return None
86 by Jaap Karssenberg
* Added documents folder and attachments folder actions
996
95 by Jaap Karssenberg
Fixed new page, delete page & attach file dialogs
997
	def get_template(self, path):
998
		'''Returns a template object for path. Typically used to set initial
999
		content for a new page.
1000
		'''
1001
		from zim.templates import get_template
121.1.6 by Jaap Karssenberg
Added special template for Calendar pages
1002
		template = self.namespace_properties[path].get('template', '_New')
1003
		logger.debug('Found template \'%s\' for %s', template, path)
1004
		return get_template('wiki', template)
95 by Jaap Karssenberg
Fixed new page, delete page & attach file dialogs
1005
59 by Jaap Karssenberg
Refactored notebook API around db index - tests OK, GUI still broken
1006
	def walk(self, path=None):
1007
		'''Generator function which iterates through all pages, depth first.
1008
		If a path is given, only iterates through sub-pages of that path.
66 by pardus
LinkMap now understands the index :)
1009
65 by Jaap Karssenberg
sync - partial broken
1010
		If you are only interested in the paths using Index.walk() will be
1011
		more efficient.
59 by Jaap Karssenberg
Refactored notebook API around db index - tests OK, GUI still broken
1012
		'''
1013
		if path == None:
1014
			path = Path(':')
65 by Jaap Karssenberg
sync - partial broken
1015
		for p in self.index.walk(path):
1016
			page = self.get_page(p)
59 by Jaap Karssenberg
Refactored notebook API around db index - tests OK, GUI still broken
1017
			yield page
1018
1019
	def get_pagelist_indexkey(self, path):
73 by pardus
Index slightly more robust...
1020
		store = self.get_store(path)
1021
		return store.get_pagelist_indexkey(path)
59 by Jaap Karssenberg
Refactored notebook API around db index - tests OK, GUI still broken
1022
1023
	def get_page_indexkey(self, path):
73 by pardus
Index slightly more robust...
1024
		store = self.get_store(path)
1025
		return store.get_page_indexkey(path)
59 by Jaap Karssenberg
Refactored notebook API around db index - tests OK, GUI still broken
1026
89 by pardus
Basic move & delete page implemented
1027
# Need to register classes defining gobject signals
1028
gobject.type_register(Notebook)
1029
86 by Jaap Karssenberg
* Added documents folder and attachments folder actions
1030
59 by Jaap Karssenberg
Refactored notebook API around db index - tests OK, GUI still broken
1031
class Path(object):
1032
	'''This is the parent class for the Page class. It contains the name
1033
	of the page and is used instead of the actual page object by methods
97 by Jaap Karssenberg
Implemented logic for saving pages and related error handling
1034
	that only know the name of the page. Path objects have no internal state
1035
	and are essentially normalized page names.
59 by Jaap Karssenberg
Refactored notebook API around db index - tests OK, GUI still broken
1036
	'''
1037
1038
	__slots__ = ('name',)
1039
1040
	def __init__(self, name):
1041
		'''Constructor. Takes an absolute page name in the right case.
1042
		The name ":" is used as a special case to construct a path for
1043
		the toplevel namespace in a notebook.
1044
1045
		Note: This class does not do any checks for the sanity of the path
1046
		name. Never construct a path directly from user input, but always use
1047
		"Notebook.resolve_path()" for that.
1048
		'''
72 by pardus
sync work on index
1049
		if isinstance(name, (list, tuple)):
1050
			name = ':'.join(name)
1051
59 by Jaap Karssenberg
Refactored notebook API around db index - tests OK, GUI still broken
1052
		if name == ':': # root namespace
1053
			self.name = ''
1054
		else:
60 by Jaap Karssenberg
PageTreeStore is now working correctly
1055
			self.name = name.strip(':')
59 by Jaap Karssenberg
Refactored notebook API around db index - tests OK, GUI still broken
1056
1057
	def __repr__(self):
1058
		return '<%s: %s>' % (self.__class__.__name__, self.name)
1059
1060
	def __eq__(self, other):
1061
		'''Paths are equal when their names are the same'''
1062
		if isinstance(other, Path):
1063
			return self.name == other.name
1064
		else: # e.g. path == None
1065
			return False
1066
95 by Jaap Karssenberg
Fixed new page, delete page & attach file dialogs
1067
	def __ne__(self, other):
1068
		return not self.__eq__(other)
1069
111.1.1 by Jaap Karssenberg
Added rough calendar plugin
1070
	def __lt__(self, other):
1071
		'''`self < other` evaluates True when self is a parent of other'''
115 by Jaap Karssenberg
Added option for showing the calendar embedded in the sidepane
1072
		return self.isroot or other.name.startswith(self.name+':')
111.1.1 by Jaap Karssenberg
Added rough calendar plugin
1073
121.1.8 by Jaap Karssenberg
Implemented updatign links on move
1074
	def __le__(self, other):
1075
		'''`self <= other` is True if `self == other or self < other`'''
1076
		return self.__eq__(other) or self.__lt__(other)
1077
111.1.1 by Jaap Karssenberg
Added rough calendar plugin
1078
	def __gt__(self, other):
1079
		'''`self > other` evaluates True when self is a child of other'''
115 by Jaap Karssenberg
Added option for showing the calendar embedded in the sidepane
1080
		return other.isroot or self.name.startswith(other.name+':')
111.1.1 by Jaap Karssenberg
Added rough calendar plugin
1081
121.1.8 by Jaap Karssenberg
Implemented updatign links on move
1082
	def __ge__(self, other):
1083
		'''`self >= other` is True if `self == other or self > other`'''
1084
		return self.__eq__(other) or self.__gt__(other)
1085
59 by Jaap Karssenberg
Refactored notebook API around db index - tests OK, GUI still broken
1086
	def __add__(self, name):
116 by Jaap Karssenberg
Moved test data to XML file to avoid issues with utf-8 filenames after unzip
1087
		'''"path + name" is an alias for path.child(name)'''
1088
		return self.child(name)
59 by Jaap Karssenberg
Refactored notebook API around db index - tests OK, GUI still broken
1089
1090
	@property
72 by pardus
sync work on index
1091
	def parts(self):
1092
		return self.name.split(':')
1093
1094
	@property
59 by Jaap Karssenberg
Refactored notebook API around db index - tests OK, GUI still broken
1095
	def basename(self):
1096
		i = self.name.rfind(':') + 1
1097
		return self.name[i:]
1098
1099
	@property
1100
	def namespace(self):
1101
		'''Gives the name for the parent page.
1102
		Returns an empty string for the top level namespace.
1103
		'''
1104
		i = self.name.rfind(':')
1105
		if i > 0:
1106
			return self.name[:i]
1107
		else:
1108
			return ''
1109
1110
	@property
1111
	def isroot(self):
1112
		return self.name == ''
1113
1114
	def relname(self, path):
1115
		'''Returns a relative name for this path compared to the reference.
1116
		Raises an error if this page is not below the given path.
1117
		'''
1118
		if path.name == '': # root path
1119
			return self.name
1120
		elif self.name.startswith(path.name + ':'):
1121
			i = len(path.name)+1
1122
			return self.name[i:]
1123
		else:
1124
			raise Exception, '"%s" is not below "%s"' % (self, path)
1125
116 by Jaap Karssenberg
Moved test data to XML file to avoid issues with utf-8 filenames after unzip
1126
	@property
1127
	def parent(self):
66 by pardus
LinkMap now understands the index :)
1128
		'''Returns the path for the parent page'''
1129
		namespace = self.namespace
1130
		if namespace:
1131
			return Path(namespace)
1132
		elif self.isroot:
1133
			return None
1134
		else:
1135
			return Path(':')
1136
59 by Jaap Karssenberg
Refactored notebook API around db index - tests OK, GUI still broken
1137
	def parents(self):
1138
		'''Generator function for parent namespace paths including root'''
60 by Jaap Karssenberg
PageTreeStore is now working correctly
1139
		if ':' in self.name:
1140
			path = self.name.split(':')
59 by Jaap Karssenberg
Refactored notebook API around db index - tests OK, GUI still broken
1141
			path.pop()
60 by Jaap Karssenberg
PageTreeStore is now working correctly
1142
			while len(path) > 0:
1143
				namespace = ':'.join(path)
1144
				yield Path(namespace)
1145
				path.pop()
59 by Jaap Karssenberg
Refactored notebook API around db index - tests OK, GUI still broken
1146
		yield Path(':')
1147
1148
116 by Jaap Karssenberg
Moved test data to XML file to avoid issues with utf-8 filenames after unzip
1149
	def child(self, name):
1150
		'''Returns a child path for 'name' '''
1151
		if len(self.name):
1152
			return Path(self.name+':'+name)
1153
		else: # we are the top level root namespace
1154
			return Path(name)
1155
121.1.8 by Jaap Karssenberg
Implemented updatign links on move
1156
	def commonparent(self, other):
1157
		parent = []
1158
		parts = self.parts
1159
		other = other.parts
1160
		if parts[0] != other[0]:
1161
			return Path(':') # root
1162
		else:
1163
			for i in range(min(len(parts), len(other))):
1164
				if parts[i] == other[i]:
1165
					parent.append(parts[i])
1166
				else:
1167
					return Path(':'.join(parent))
1168
			else:
1169
				return Path(':'.join(parent))
1170
116 by Jaap Karssenberg
Moved test data to XML file to avoid issues with utf-8 filenames after unzip
1171
59 by Jaap Karssenberg
Refactored notebook API around db index - tests OK, GUI still broken
1172
class Page(Path):
105 by Jaap Karssenberg
Various clean ups
1173
	'''Class to represent a single page in the notebook.
59 by Jaap Karssenberg
Refactored notebook API around db index - tests OK, GUI still broken
1174
97 by Jaap Karssenberg
Implemented logic for saving pages and related error handling
1175
	Page objects inherit from Path but have internal state reflecting content
1176
	in the notebook. We try to keep Page objects unique
59 by Jaap Karssenberg
Refactored notebook API around db index - tests OK, GUI still broken
1177
	by hashing them in notebook.get_page(), Path object on the other hand
1178
	are cheap and can have multiple instances for the same logical path.
1179
	We ask for a path object instead of a name in the constructore to
1180
	encourage the use of Path objects over passsing around page names as
1181
	string. Also this allows some optimalizations by addind index pointers
1182
	to the Path instances.
97 by Jaap Karssenberg
Implemented logic for saving pages and related error handling
1183
1184
	You can use a Page object instead of a Path anywhere in the APIs where
1185
	a path is needed as argument etc.
114 by Jaap Karssenberg
Fixes for the refactoring of Dialog to zim.gui.widgets
1186
1187
	Page objects have an attribute 'valid' which should evaluate True. If for
1188
	some reason this object is abandoned by the notebook, this attribute will
1189
	be set to False. Once the page object is invalidated you can no longer use
1190
	it's internal state. However in that case the object can still be used as
1191
	a regular Path object to point to the location of a page. The way replace
1192
	an invalid page object is by calling `notebook.get_page(invalid_page)`.
59 by Jaap Karssenberg
Refactored notebook API around db index - tests OK, GUI still broken
1193
	'''
4 by Jaap Karssenberg
Added classes Notebook, Page and PageList
1194
78 by Jaap Karssenberg
Moved file based page code to store.files.FileStorePage
1195
	def __init__(self, path, haschildren=False, parsetree=None):
1196
		'''Construct Page object. Needs a path object and a boolean to flag
1197
		if the page has children.
4 by Jaap Karssenberg
Added classes Notebook, Page and PageList
1198
		'''
59 by Jaap Karssenberg
Refactored notebook API around db index - tests OK, GUI still broken
1199
		assert isinstance(path, Path)
1200
		self.name = path.name
1201
		self.haschildren = haschildren
97 by Jaap Karssenberg
Implemented logic for saving pages and related error handling
1202
		self.valid = True
1203
		self.modified = False
78 by Jaap Karssenberg
Moved file based page code to store.files.FileStorePage
1204
		self._parsetree = parsetree
97 by Jaap Karssenberg
Implemented logic for saving pages and related error handling
1205
		self._ui_object = None
154 by Jaap Karssenberg
* Added --fullscreen and --geometry options
1206
		self.readonly = True # stores need to explicitly set readonly False
52 by Jaap Karssenberg
www index pages now also use the template
1207
		self.properties = {}
59 by Jaap Karssenberg
Refactored notebook API around db index - tests OK, GUI still broken
1208
1209
	@property
1210
	def hascontent(self):
78 by Jaap Karssenberg
Moved file based page code to store.files.FileStorePage
1211
		'''Returns whether this page has content'''
176 by Jaap Karssenberg
* Improved page update on delete & move
1212
		if self._parsetree:
1213
			return self._parsetree.hascontent
1214
		elif self._ui_object:
1215
			return self._ui_object.get_parsetree().hascontent
1216
		else:
1217
			try:
1218
				hascontent = self._source_hascontent()
1219
			except NotImplementedError:
1220
				return False
1221
			else:
1222
				return hascontent
5 by Jaap Karssenberg
Server can now walk trough page index
1223
24 by Jaap Karssenberg
Setup Application framework
1224
	def get_parsetree(self):
97 by Jaap Karssenberg
Implemented logic for saving pages and related error handling
1225
		'''Returns contents as a parsetree or None'''
1226
		assert self.valid, 'BUG: page object became invalid'
1227
1228
		if self._parsetree:
1229
			return self._parsetree
1230
		elif self._ui_object:
1231
			return self._ui_object.get_parsetree()
1232
		else:
1233
			try:
1234
				self._parsetree = self._fetch_parsetree()
1235
			except NotImplementedError:
1236
				return None
1237
			else:
1238
				return self._parsetree
1239
176 by Jaap Karssenberg
* Improved page update on delete & move
1240
	def _source_hascontent(self):
1241
		'''Method to be overloaded in sub-classes.
1242
		Should return a parsetree True if _fetch_parsetree() returns content.
1243
		'''
1244
		raise NotImplementedError
1245
97 by Jaap Karssenberg
Implemented logic for saving pages and related error handling
1246
	def _fetch_parsetree(self):
176 by Jaap Karssenberg
* Improved page update on delete & move
1247
		'''Method to be overloaded in sub-classes.
1248
		Should return a parsetree or None.
97 by Jaap Karssenberg
Implemented logic for saving pages and related error handling
1249
		'''
1250
		raise NotImplementedError
4 by Jaap Karssenberg
Added classes Notebook, Page and PageList
1251
24 by Jaap Karssenberg
Setup Application framework
1252
	def set_parsetree(self, tree):
78 by Jaap Karssenberg
Moved file based page code to store.files.FileStorePage
1253
		'''Set the parsetree with content for this page. Set the parsetree
1254
		to None to remove all content.
1255
		'''
114 by Jaap Karssenberg
Fixes for the refactoring of Dialog to zim.gui.widgets
1256
		assert self.valid, 'BUG: page object became invalid'
1257
121.1.2 by Jaap Karssenberg
Implemented read-only state
1258
		if self.readonly:
1259
			raise PageReadOnlyError, self
97 by Jaap Karssenberg
Implemented logic for saving pages and related error handling
1260
1261
		if self._ui_object:
1262
			self._ui_object.set_parsetree(tree)
1263
		else:
1264
			self._parsetree = tree
1265
1266
		self.modified = True
1267
1268
	def set_ui_object(self, object):
1269
		'''Set a temporary hook to fetch the parse tree. Used by the gtk ui to
1270
		'lock' pages that are being edited. Set to None to break the lock.
1271
1272
		The ui object should in turn have a get_parsetree() and a
1273
		set_parsetree() method which will be called by the page object.
1274
		'''
1275
		if object is None:
1276
			self._parsetree = self._ui_object.get_parsetree()
1277
			self._ui_object = None
1278
		else:
1279
			assert self._ui_object is None, 'BUG: page already being edited by another widget'
1280
			self._parsetree = None
1281
			self._ui_object = object
78 by Jaap Karssenberg
Moved file based page code to store.files.FileStorePage
1282
96 by Jaap Karssenberg
updated exportdialog and introduced Linker objects
1283
	def dump(self, format, linker=None):
78 by Jaap Karssenberg
Moved file based page code to store.files.FileStorePage
1284
		'''Convenience method that converts the current parse tree to a
1285
		particular format and returns a list of lines. Format can be either a
1286
		format module or a string which can be passed to formats.get_format().
59 by Jaap Karssenberg
Refactored notebook API around db index - tests OK, GUI still broken
1287
		'''
78 by Jaap Karssenberg
Moved file based page code to store.files.FileStorePage
1288
		if isinstance(format, basestring):
1289
			import zim.formats
1290
			format = zim.formats.get_format(format)
1291
96 by Jaap Karssenberg
updated exportdialog and introduced Linker objects
1292
		if not linker is None:
1293
			linker.set_path(self)
1294
24 by Jaap Karssenberg
Setup Application framework
1295
		tree = self.get_parsetree()
5 by Jaap Karssenberg
Server can now walk trough page index
1296
		if tree:
96 by Jaap Karssenberg
updated exportdialog and introduced Linker objects
1297
			return format.Dumper(linker=linker).dump(tree)
5 by Jaap Karssenberg
Server can now walk trough page index
1298
		else:
77 by Jaap Karssenberg
* Removed the Buffer class
1299
			return []
5 by Jaap Karssenberg
Server can now walk trough page index
1300
78 by Jaap Karssenberg
Moved file based page code to store.files.FileStorePage
1301
	def parse(self, format, text):
1302
		'''Convenience method that parses text and sets the parse tree
1303
		for this page. Format can be either a format module or a string which
1304
		can be passed to formats.get_format(). Text can be either a string or
1305
		a list or iterable of lines.
56 by Jaap Karssenberg
Structural improvements of the template module and related code
1306
		'''
78 by Jaap Karssenberg
Moved file based page code to store.files.FileStorePage
1307
		if isinstance(format, basestring):
1308
			import zim.formats
1309
			format = zim.formats.get_format(format)
1310
1311
		self.set_parsetree(format.Parser().parse(text))
56 by Jaap Karssenberg
Structural improvements of the template module and related code
1312
59 by Jaap Karssenberg
Refactored notebook API around db index - tests OK, GUI still broken
1313
	def get_links(self):
121.1.8 by Jaap Karssenberg
Implemented updatign links on move
1314
		'''Generator for a list of tuples of type, href and attrib for links
1315
		in the parsetree.
1316
1317
		This gives the raw links, if you want nice Link objects use
1318
		index.list_links() instead.
1319
		'''
65 by Jaap Karssenberg
sync - partial broken
1320
		tree = self.get_parsetree()
1321
		if tree:
1322
			for tag in tree.getiterator('link'):
121.1.8 by Jaap Karssenberg
Implemented updatign links on move
1323
				attrib = tag.attrib.copy()
168.1.3 by Jaap Karssenberg
Fix for "href" bug
1324
				href = attrib.pop('href')
121.1.8 by Jaap Karssenberg
Implemented updatign links on move
1325
				type = link_type(href)
1326
				yield type, href, attrib
65 by Jaap Karssenberg
sync - partial broken
1327
1328
96 by Jaap Karssenberg
updated exportdialog and introduced Linker objects
1329
class IndexPage(Page):
1330
	'''Page displaying a namespace index'''
1331
1332
	def __init__(self, notebook, path=None, recurs=True):
1333
		'''Constructor takes a namespace path'''
1334
		if path is None:
1335
			path = Path(':')
1336
		Page.__init__(self, path, haschildren=True)
1337
		self.index_recurs = recurs
1338
		self.notebook = notebook
1339
		self.properties['type'] = 'namespace-index'
1340
1341
	@property
1342
	def hascontent(self): return True
1343
1344
	def get_parsetree(self):
1345
		if self._parsetree is None:
1346
			self._parsetree = self._generate_parsetree()
1347
		return self._parsetree
1348
1349
	def _generate_parsetree(self):
1350
		import zim.formats
1351
		builder = zim.formats.TreeBuilder()
1352
1353
		def add_namespace(path):
1354
			pagelist = self.notebook.index.list_pages(path)
1355
			builder.start('ul')
1356
			for page in pagelist:
1357
				builder.start('li')
1358
				builder.start('link', {'type': 'page', 'href': page.name})
1359
				builder.data(page.basename)
1360
				builder.end('link')
1361
				builder.end('li')
1362
				if page.haschildren and self.index_recurs:
1363
					add_namespace(page) # recurs
1364
			builder.end('ul')
1365
1366
		builder.start('page')
1367
		builder.start('h', {'level':1})
1368
		builder.data('Index of %s' % self.name)
1369
		builder.end('h')
1370
		add_namespace(self)
1371
		builder.end('page')
1372
1373
		return zim.formats.ParseTree(builder.close())
1374
1375
65 by Jaap Karssenberg
sync - partial broken
1376
class Link(object):
1377
1378
	__slots__ = ('source', 'href', 'type')
1379
1380
	def __init__(self, source, href, type=None):
1381
		self.source = source
1382
		self.href = href
1383
		self.type = type
66 by pardus
LinkMap now understands the index :)
1384
1385
	def __repr__(self):
84 by pardus
Added backlinks button in statusbar
1386
		return '<%s: %s to %s (%s)>' % (self.__class__.__name__, self.source, self.href, self.type)