#!/usr/bin/env python2 # -*- coding: utf-8 -*- u"""%(scriptname)s: gérer un TiddlyWiki USAGE %(scriptname)s [options] addtext|addfile|remove|list|edit OPTIONS -c twrc Charger le fichier de configuration twrc au lieu de la valeur par défaut (~/.utools/twrc) -f twfile.html Utiliser twfile.html au lieu de chercher un fichier par défaut dans le répertoire courant. Les noms considérés par défaut sont TODO.html, project_wiki.html et TiddlyWiki.html""" __all__ = ('Tiddler', 'TiddlyWiki', 'TwCLI') import os, sys, re from os import path from getopt import getopt from time import localtime from types import IntType, LongType, StringTypes from xml.dom.minidom import parseString from pyutools.config import ShConfigFile from pyutools.dates import datef, TW_DATEF from pyutools.iso8859 import quote_attr, quote_html, unquote_but_html from pyutools import scriptdir, scriptname, dict, isnum, isstr, isseq from pyutools import get_stdin_encoding, get_editor_encoding from pyutools import ensure_unicode, uprint, enote, eerror, die, get_colored from pyutools import edit_template _marker = [] TW_PARAM_PATTERN = re.compile( r'(?:' + r'(?:"((?:(?:\\")|[^"])+)")|' + # "value" r"(?:'((?:(?:\\')|[^'])+)')|" + # 'value' r'(?:\[\[(.*?)\]\])|' + # [[space separated values]] r'(?:(\{\{.*?\}\}))|' + # {{braced values}} r'(?:([^"' + "'" + r'\s]\S*))|' + # value r'(?:"")|' + # "" r"(?:'')" + # '' r')' ) def twParseParams(s): params = [] for a, b, c, d, e in TW_PARAM_PATTERN.findall(s): value = a or b or c or d or e if value: params.append(value) return params def twParseTags(s): tags = {} for tag in twParseParams(s): tags[tag] = None return tags.keys() TW_BRACED_VALUE = re.compile(r'\{\{.*\}\}$') TW_VALUE = re.compile(r'[^"' + "'" + r'\s]\S*$') def twFormatTags(tags): ss = [] for tag in tags: if TW_BRACED_VALUE.match(tag): ss.append(tag) elif TW_VALUE.match(tag): ss.append(tag) else: ss.append("[[%s]]" % tag) return " ".join(ss) class Tiddler: parent = None # Instance de TiddlyWiki parent de ce Tiddler title = u'' modifier = u'' creation_date = None modification_date = None tags = None attrs = None changecount = 0 text = u'' modified = False def __init__(self, element=None, parent=None): self.creation_date = self.modification_date = datef(TW_DATEF) self.tags = [] self.attrs = {} if parent is not None: self.parent = parent if element is not None: self._parseElement(element) ATTRS = ['title', 'modifier', 'modification_date', 'tags', 'creation_date', 'changecount'] ATTRS1_MAP = {'tiddler': 'title', 'modified': 'modification_date'} ATTRS1_REVMAP = {'title': 'tiddler', 'modification_date': 'modified', 'creation_date': None, 'changecount': None} ATTRS2_MAP = {'modified': 'modification_date', 'created': 'creation_date'} ATTRS2_REVMAP = {'modification_date': 'modified', 'creation_date': 'created'} def __unquote1(self, s): s = s.replace('\\n', '\n') return s def __quote1(self, s): s = s.replace('\n', '\\n') return s def __parseElement1(self, div, attrs): if attrs is not None: for i in range(attrs.length): name = div.attributes.item(i).name value = div.attributes.item(i).value name = self.ATTRS1_MAP.get(name, name) if name == 'tags': # cas particulier value = twParseTags(value) if name in self.ATTRS: setattr(self, name, value) else: self.attrs[name] = value textNode = div.firstChild if textNode is not None: self.text = self.__unquote1(textNode.data) def __formatElement1(self): s = u' pos = pose + len(self.END_DIV) else: # end_save_area break pos = data.find(self.END_DIV, pos) if pos == -1: raise ValueError("%s: Impossible de trouver la fin de la zone de stockage" % self.file) self.suffix = data[pos:] data = data[:pos] return True, data except: if raise_exception: raise return False, None def load(self, file=None, raise_exception=True): if file is not None: self.__update_file(file) self.valid = False data = self.__load_prefix_and_suffix(self.pf, raise_exception)[1] # charger les données self.valid = True self.byname = {} self.tiddlers = [] data = self.START_SAVE_AREA + data + self.END_DIV data = unquote_but_html(data, "utf-8") # HACK: nécessaire car minidom # ne supporte pas les entities. utf-8 est codé en dur parce qu'on sait que # TiddlyWiki enregistre dans ce codec. dom = parseString(data) for node in dom.documentElement.getElementsByTagName('div'): self.add(Tiddler(element=node, parent=self)) return True def is_valid(self): return self.valid def save(self, file=None, templatefile=None, set_modified=True, raise_exception=True): if templatefile is None and self.file is None: templatefile = path.join(path.split(__file__)[0], 'empty.html') if templatefile is None: if not hasattr(self, 'prefix') or not hasattr(self, 'suffix'): raise IOError("Etat inconsistant: il faut le préfixe et le suffixe") else: self.__load_prefix_and_suffix(templatefile, raise_exception) if file is not None: self.__update_file(file) tmppf = self.pf + '.tmp' try: outf = open(tmppf, 'wb') try: outf.write(self.prefix) for tiddler in self.tiddlers: if tiddler.is_modified() and set_modified: tiddler.set_modified() outf.write(tiddler._formatElement().encode("utf-8")) outf.write("\n") outf.write(self.suffix) finally: outf.close() os.rename(tmppf, self.pf) except: if raise_exception: raise def set_version(self, version): self.version = version def get_version(self): return self.version def __len__(self): return len(self.tiddlers) def has_key(self, title): return self.byname.has_key(title) def __getitem__(self, indexOrTitle, default=_marker): if isnum(indexOrTitle): return self.tiddlers[indexOrTitle] else: if default is _marker: return self.byname[indexOrTitle] else: return self.byname.get(indexOrTitle, default) get = __getitem__ def add(self, tiddler): if not isinstance(tiddler, Tiddler): raise ValueError("value doit être une instance de Tiddler") title = tiddler.get_title() if title in self.byname: del self.tiddlers[self.byname[title]] tiddler.parent = self self.tiddlers.append(tiddler) self.byname[title] = tiddler def __delitem__(self, indexOrTitle): if isnum(indexOrTitle): tiddler = self.tiddlers[indexOrTitle] index = indexOrTitle else: tiddler = self.byname[indexOrTitle] index = self.tiddlers.index(tiddler) del self.byname[tiddler.get_title()] del self.tiddlers[index] def __repr__(self): return repr(map(lambda t: t.get_title(), self.tiddlers)) ################################################################################ class TwrcFile(ShConfigFile): TWRC = "twrc" def __init__(self, file=None, raise_exception=True): if file is None: testdir = path.join(scriptdir, "test") utoolsrcdir = path.join(path.expanduser("~"), ".utools") if path.isdir(testdir): file = path.join(testdir, self.TWRC) else: file = path.join(utoolsrcdir, self.TWRC) raise_exception = False ShConfigFile.__init__(self, file=file, raise_exception=raise_exception) DEFAULT_NAMES = "default_names" MODIFIER = "modifier" def __p(self, p, refdir=None): if refdir is not None: if not path.isdir(refdir): refdir = path.split(refdir)[0] p = path.normpath(path.expanduser(p)) if refdir is not None: p = path.join(refdir, p) return p COMMA_PATTERN = re.compile(r'\s*,\s*') def __vs(self, vs): if isstr(vs): vs = self.COMMA_PATTERN.split(vs) return tuple(vs) def __csv(self, csv): if isseq(csv): csv = ','.join(csv) return ensure_unicode(csv) def get_default_names(self): if self.has_key(self.DEFAULT_NAMES): return self.__vs(self[self.DEFAULT_NAMES]) else: return TiddlyWiki.DEFAULT_NAMES def set_default_names(self, default_names=None): if default_names is None: if self.has_key(self.DEFAULT_NAMES): del self[self.DEFAULT_NAMES] else: self[self.DEFAULT_NAMES] = self.__csv(default_names) def get_modifier(self): if self.has_key(self.MODIFIER): return self[self.MODIFIER] else: return os.environ.get('USER', 'TiddlyWiki.py') def set_modifier(self, modifier=None): if modifier is None: if self.has_key(self.MODIFIER): del self[self.MODIFIER] else: self[self.MODIFIER] = modifier class TwCLI: twrc = None def __newTiddlyWiki(self, file=None): return TiddlyWiki(file, default_names=self.twrc.get_default_names(), raise_exception=True) twfile = None def __twfile(self): if self.twfile is None: self.twfile = self.__newTiddlyWiki() return self.twfile def __init__(self, twrc=None): if not isinstance(twrc, TwrcFile): twrc = TwrcFile(twrc) self.twrc = twrc CONFOPT = 'c:f:' CONFLOPT = ['config=', 'file='] def is_global_option(self, opt, value): if opt in ('-c', '--config'): self.twrc = TwrcFile(value) elif opt in ('-f', '--file'): self.twfile = self.__newTiddlyWiki(value) else: return False return True def ADDTEXT(self, title=None, text=None, modifier=None, set_tags=None, add_tags=None, remove_tags=None, encoding=None, argv=(), scriptname=None, **kw): u"""%(scriptname)s: Créer ou mettre à jour un tiddler USAGE %(scriptname)s [options] title OPTIONS -m text Si le texte du tiddler est spécifié, on ne lance pas d'éditeur -u modifier Spécifier le nom de l'utilisateur qui fait la modification. Par défaut, il s'agit de $USER -t tag tag: ajouter un tag, -tag: enlever un tag -e encoding Spécifier l'encoding du titre et du texte s'il sont spécifiés sur la ligne de commande. Par défaut, on considère que les données sont encodées en %(default_encoding)s""" default_encoding = get_stdin_encoding() opts, argv = getopt(argv, self.CONFOPT + 'hm:u:t:e:', self.CONFLOPT + ['help', 'text=', 'modifier=', 'tag=', 'encoding=']) for opt, value in opts: if self.is_global_option(opt, value): pass elif opt in ('-h', '--help'): uprint(self.LIST.__doc__ % locals()) sys.exit(0) elif opt in ('-m', '--text'): text = value elif opt in ('-u', '--modifier'): modifier = value elif opt in ('-t', '--tag'): if value.startswith('-'): if remove_tags is None: remove_tags = [] remove_tags.append(value[1:]) else: if value.startswith('+'): value = value[1:] if add_tags is None: add_tags = [] add_tags.append(value) elif opt in ('-e', '--encoding'): encoding = value if title is None: if not argv: raise ValueError("Il faut spécifier un titre pour le nouveau tiddler") title = argv[0] edit = False if text is None: text = '' edit = True if modifier is None: modifier = self.twrc.get_modifier() if encoding is None: encoding = default_encoding title = ensure_unicode(title, encoding) text = ensure_unicode(text, encoding) twfile = self.__twfile() new_tiddler = False tiddler = twfile.get(title, None) if tiddler is None: new_tiddler = True tiddler = Tiddler() twfile.add(tiddler) tiddler.set_title(title) tiddler.set_text(text) tiddler.set_modifier(modifier) if set_tags is not None: tiddler.set_tags(set_tags) if add_tags is not None: for tag in add_tags: tiddler.add_tag(tag) if remove_tags is not None: for tag in remove_tags: tiddler.remove_tag(tag) if tiddler.is_modified(): twfile.save() if edit: self.EDIT(tiddler=tiddler) else: if new_tiddler: enote(u"Ajout d'un nouveau tiddler '%s'" % title) else: enote(u"Mise à jour du tiddler '%s'" % title) def ADDFILE(self, file=None, title=None, modifier=None, set_tags=None, add_tags=None, remove_tags=None, encoding=None, argv=(), scriptname=None, **kw): u"""%(scriptname)s: Créer ou mettre à jour un tiddler à partir d'un fichier USAGE %(scriptname)s [options] /path/to/file OPTIONS -n title Spécifier le titre du tiddler. Par défaut, il s'agit du nom du fichier -u modifier Spécifier le nom de l'utilisateur qui fait la modification. Par défaut, il s'agit de $USER -t tag Ajouter un tag Si le fichier a l'extension .js, on ajoute automatiquement le tag systemConfig, sauf si un tag est spécifié -e encoding Spécifier l'encoding du fichier. Par défaut, on lit en %(default_encoding)s""" default_encoding = get_editor_encoding() opts, argv = getopt(argv, self.CONFOPT + 'hn:u:t:e:', self.CONFLOPT + ['help', 'title=', 'modifier=', 'tag=', 'encoding=']) for opt, value in opts: if self.is_global_option(opt, value): pass elif opt in ('-h', '--help'): uprint(self.LIST.__doc__ % locals()) sys.exit(0) elif opt in ('-n', '--title'): title = value elif opt in ('-u', '--modifier'): modifier = value elif opt in ('-t', '--tag'): if value.startswith('-'): if remove_tags is None: remove_tags = [] remove_tags.append(value[1:]) else: if value.startswith('+'): value = value[1:] if add_tags is None: add_tags = [] add_tags.append(value) elif opt in ('-e', '--encoding'): encoding = value if file is None: if not argv: raise ValueError("Il faut spécifier un fichier à importer") file = argv[0] if not path.exists(file): raise IOError("Fichier inexistant: %s" % file) if title is None: title = file if modifier is None: modifier = os.environ.get('USER', 'TiddlyWiki.py') if path.splitext(file)[1] == '.js': if set_tags is not None: set_tags.append('systemConfig') else: if add_tags is None: add_tags = [] add_tags.append('systemConfig') if encoding is None: encoding = defaut_encoding inf = open(file, 'rb') try: text = ensure_unicode(inf.read(), encoding) finally: inf.close() twfile = self.__twfile() new_tiddler = False tiddler = twfile.get(title, None) if tiddler is None: new_tiddler = True tiddler = Tiddler() twfile.add(tiddler) tiddler.set_title(title) tiddler.set_modifier(modifier) if set_tags is not None: tiddler.set_tags(set_tags) if add_tags is not None: for tag in add_tags: tiddler.add_tag(tag) if remove_tags is not None: for tag in remove_tags: tiddler.remove_tag(tag) tiddler.set_text(text) if tiddler.is_modified(): twfile.save() if new_tiddler: enote(u"Ajout d'un nouveau tiddler '%s'" % title) else: enote(u"Mise à jour du tiddler '%s'" % title) def REMOVE(self, title=None, argv=(), scriptname=None, **kw): u"""%(scriptname)s: Supprimer un tiddler USAGE %(scriptname)s title""" opts, argv = getopt(argv, self.CONFOPT + 'hn:', self.CONFLOPT + ['help', 'title=']) for opt, value in opts: if self.is_global_option(opt, value): pass elif opt in ('-h', '--help'): uprint(self.LIST.__doc__ % locals()) sys.exit(0) elif opt in ('-n', '--title'): title = value if title is None: if not argv: raise ValueError("Il faut spécifier le tiddler à supprimer") title = argv[0] twfile = self.__twfile() if twfile.has_key(title): del twfile[title] twfile.save() enote(u"Suppression du tiddler '%s'" % title) def LIST(self, showtext=False, argv=(), scriptname=None, **kw): u"""%(scriptname)s: Lister les tiddlers USAGE %(scriptname)s OPTIONS -l Afficher aussi le contenu des tiddlers""" opts, argv = getopt(argv, self.CONFOPT + 'hl', self.CONFLOPT + ['help', 'show-text']) for opt, value in opts: if self.is_global_option(opt, value): pass elif opt in ('-h', '--help'): uprint(self.LIST.__doc__ % locals()) sys.exit(0) elif opt in ('-l', '--show-text'): showtext = True twfile = self.__twfile() if showtext: for tiddler in twfile: title = get_colored(u'>>> ' + tiddler.get_title(), 'b') if tiddler.get_tags(): title += " [%s]" % ', '.join(map(lambda t: get_colored(tiddler, 'y'), tiddler.get_tags())) uprint(title) text = tiddler.get_text() if text: uprint(text) else: for tiddler in twfile: uprint(tiddler.get_title()) EDIT_TEMPLATE = u""" EDIT: ---------------------------------------------------------------- EDIT: Saisissez ou modifiez le titre et le corps du tiddler. EDIT: EDIT: - Les lignes commencant par 'EDIT:' seront supprimées automatiquement EDIT: - La ligne tags: peut être modifiée si nécessaire. EDIT: EDIT: ----------------------------------------------------------------""" TAGS_PATTERN = re.compile(r'##\s*tags:\s*') def __nblines(self, s): lines = s.split("\n") nblines = len(lines) if not lines[-1]: nblines -= 1 return nblines def EDIT(self, title=None, tiddler=None, argv=(), scriptname=None, **kw): u"""%(scriptname)s: Editer un tiddler USAGE %(scriptname)s title""" opts, argv = getopt(argv, self.CONFOPT + 'h', self.CONFLOPT + ['help']) for opt, value in opts: if self.is_global_option(opt, value): pass elif opt in ('-h', '--help'): uprint(self.LIST.__doc__ % locals()) sys.exit(0) twfile = self.__twfile() if tiddler is None: if title is None: if not argv: raise ValueError("Il faut spécifier le tiddler à éditer") title = argv[0] tiddler = twfile.get(title, None) if tiddler is None: raise ValueError("Tiddler non trouvé: %s" % title) template = u"" template += u"## tags: %s\n" % twFormatTags(tiddler.get_tags()) template += u"\n" title = tiddler.get_title() template += u"%s\n" % title setline = self.__nblines(template) setcol = len(title) text = tiddler.get_text() if text: template += u"\n%s\n" % text template += self.EDIT_TEMPLATE lines = edit_template(template, 'EDIT:', setline, setcol).split('\n') new_tags = [] parsing_tags = True skip_empty = True text = [] for line in lines: if skip_empty and not line: continue if parsing_tags: mot = self.TAGS_PATTERN.match(line) if mot is not None: new_tags.extend(twParseTags(line[mot.end():])) continue else: parsing_tags = False skip_empty = False text.append(line) text = "\n".join(text) pos = text.find('\n\n') if pos == -1: title = text.replace('\n', ' ') text = u'' else: title = text[:pos].replace('\n', ' ') text = text[pos + 2:] tiddler.set_tags(new_tags) tiddler.set_title(title) tiddler.set_text(text) if tiddler.is_modified(): twfile.save() enote(u"Mise à jour du tiddler '%s'" % title) ################################################################################ if __name__ == '__main__': debug = False action = None argv = sys.argv[1:] twCLI = TwCLI() # Essayer de determiner l'action avec le nom du script if scriptname in ('twa', ): action = 'addtext' elif scriptname in ('twf', ): action = 'addfile' elif scriptname in ('twl', 'twll',): if scriptname == 'twll': argv.insert(0, '-l') action = 'list' elif scriptname in ('twe', ): action = 'edit' if action is None: opts, argv = getopt(argv, TwCLI.CONFOPT + 'hD', TwCLI.CONFLOPT + ['help', 'debug']) for opt, value in opts: if opt in ('-h', '--help'): uprint(__doc__ % dict(scriptname=scriptname)) sys.exit(0) elif twCLI.is_global_option(opt, value): pass elif opt in ('-D', '--debug'): debug = True if not argv: uprint(__doc__ % dict(scriptname=scriptname)) sys.exit(0) action, argv = argv[0], argv[1:] if action in ('addtext', 'add', 'a'): action = 'addtext' elif action in ('addfile', 'file', 'f'): action = 'addfile' elif action in ('remove', 'r'): action = 'remove' elif action in ('list', 'l', 'll'): if action == 'll': argv.insert(0, '-l') action = 'list' elif action in ('edit', 'e'): action = 'edit' else: eerror("Action inconnue: %s" % action) sys.exit(1) if scriptname in ('TiddlyWiki.py', 'tw'): # pour l'affichage de l'aide scriptname = '%s %s' % (scriptname, action) try: apply(getattr(twCLI, action.upper()), (), {'argv': argv, 'scriptname': scriptname}) except Exception, e: if debug: eerror(e[0]) import traceback traceback.print_exc() else: die(e[0])