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) |