#!/usr/bin/python # -*- coding: utf-8 -*- # # © Copyright 2011 Jan Dittberner # # Tool for updating wiki pages on MoinMoin wikis from ReStructuredText # (ReST) sources using XML-RPC API. The tool was originally developed # for the needs of the CAcert.org community. # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see # . """ This tool updates a set of wiki pages in a MoinMoin wiki using preformatted content from ReStructuredText sources. """ from ConfigParser import ConfigParser from docutils import nodes, writers from docutils.parsers.rst import directives, Parser from docutils.utils import new_document import codecs import logging import logging.config import os.path import sys import xmlrpclib config = ConfigParser() config.readfp(open('moinupdater.ini')) config.read([os.path.expanduser('~/.cacerttools')]) logging.config.fileConfig('cacerttools.ini') class MoinRSTWriter(writers.Writer): SUPPORTED_SYNTAX = ('python', 'pascal', 'java', 'cplusplus', 'csv', 'rst') """Docutils Writer implementation writing MoinMoin wiki syntax.""" def translate(self): """Translate docutils parsed content to a MoinMoin string.""" self.visitor = visitor = MoinRSTTranslator(self.document) self.document.walkabout(visitor) blocks = [] for block in getattr(visitor, 'blocks'): blockout = u'' if block['syntax'] in self.SUPPORTED_SYNTAX: blockout += u'{{{#!%s\n' % block['syntax'] else: blockout += u'{{{\n' blockout += block['body'] blockout += u'\n}}}' blocks.append(blockout) self.output = u'\n'.join(blocks) class MoinRSTTranslator(nodes.NodeVisitor): """Translator from docutils document tree nodes to MoinMoin syntax.""" def __init__(self, document): nodes.NodeVisitor.__init__(self, document) self.context = {'indent': 0, 'list_type': [], 'bullet': [], 'sectionlevels': [], 'suppress': False, 'noindent': False} self.blocks = [] def _start_block(self, syntax): self.blocks.append({'syntax': syntax, 'body': []}) def _finish_block(self): if not self.blocks[-1]['body']: del self.blocks[-1] else: self.blocks[-1]['body'] = (u''.join(self.blocks[-1]['body'])).strip() def _add_content(self, text): if not self.context['suppress']: self.blocks[-1]['body'].append(text) def visit_decoration(self, node): pass def depart_decoration(self, node): pass def visit_docinfo(self, node): self._add_content(u'\n') def depart_docinfo(self, node): self._add_content(u'\n') def visit_substitution_definition(self, node): pass def depart_substitution_definition(self, node): pass def visit_image(self, node): self._add_content(u'.. image:: %s\n' % node.attributes['uri']) for key in [key for key in node.attributes if not key in ( 'uri', 'alt') and node.attributes[key]]: self._add_content(u' :%s: %s\n' % (key, node.attributes[key])) def depart_image(self, node): pass def visit_header(self, node): self.context['suppress'] = True def depart_header(self, node): self.context['suppress'] = False def visit_bullet_list(self, node): self._add_content(u'\n') self.context['list_type'].append('bullet') if 'bullet' in node.attributes: self.context['bullet'].append(node.attributes['bullet']) else: self.context['bullet'].append(None) def depart_bullet_list(self, node): del self.context['bullet'][-1] del self.context['list_type'][-1] def visit_list_item(self, node): self._add_content(u' ' * self.context['indent']) if self.context['list_type'][-1] == 'bullet': if self.context['bullet'][-1]: bullet = u'%s ' % self.context['bullet'][-1] self._add_content(bullet) self.context['indent'] += len(bullet) else: print 'item in unhandled list type', \ self.context['list_type'][-1], node def depart_list_item(self, node): if self.context['list_type'][-1] == 'bullet': if self.context['bullet'][-1]: bullet = u'%s ' % self.context['bullet'][-1] self.context['indent'] -= len(bullet) def visit_literal_block(self, node): self._finish_block() if 'syntax' in node.attributes: self._start_block(node.attributes['syntax']) else: self._start_block(None) self.context['noindent'] = True def depart_literal_block(self, node): self._finish_block() self._start_block('rst') self.context['noindent'] = False def visit_document(self, node): self._start_block('rst') def depart_document(self, node): self._finish_block() def visit_Text(self, node): if not self.context['noindent']: content = node.replace('\n', '\n%s' % ( " " * self.context['indent'])) else: content = node self._add_content(u'%s' % content) def depart_Text(self, node): pass def visit_literal(self, node): self._add_content(u'``') def depart_literal(self, node): self._add_content(u'``') def visit_paragraph(self, node): pass def depart_paragraph(self, node): self._add_content(u'\n') def visit_topic(self, node): if 'contents' in node.attributes['ids']: self._add_content(u'\n.. contents::\n') self.context['suppress'] = True else: print 'topic', dir(node), node.attributes def depart_topic(self, node): if 'contents' in node.attributes['ids']: self.context['suppress'] = False def visit_reference(self, node): pass def depart_reference(self, node): pass def visit_author(self, node): self._add_content(u':Author: ') def depart_author(self, node): self._add_content(u'\n') def visit_version(self, node): self._add_content(u':Version: ') def depart_version(self, node): self._add_content(u'\n') def visit_date(self, node): self._add_content(u':Date: ') def depart_date(self, node): self._add_content(u'\n') def visit_section(self, node): if node.parent.tagname == 'document': self.context['sectionlevels'].append(1) else: self.context['sectionlevels'].append( self.context['sectionlevels'][-1] + 1) self._add_content('\n') def depart_section(self, node): del self.context['sectionlevels'][-1] def visit_title(self, node): if not self.context['sectionlevels']: self._add_content(u'%s\n' % ('=' * len(node.rawsource))) def depart_title(self, node): if self.context['sectionlevels']: level = self.context['sectionlevels'][-1] self._add_content(u'\n%s\n' % (('=', '-', '~')[level - 1] \ * len(node.rawsource))) else: self._add_content(u'\n%s\n' % ('=' * len(node.rawsource))) def code_block_directive(name, arguments, options, content, lineno, content_offset, block_text, state, state_machine): if 'include' in options: content = codecs.open(options['include'], 'r', 'utf-8').read().rstrip() else: content = u'\n'.join(content) tabw = int(options.get('tab-width', 8)) content = content.replace('\t', ' ' * tabw) withln = 'linenos' in options if not 'linenos_offset' in options: line_offset = 0 language = arguments[0] code_block = nodes.literal_block(classes=['code', language], syntax=language) if withln: lineno = 1 + line_offset total_lines = content.count('\n') + 1 + line_offset lnwidth = len(str(total_lines)) fstr = "\n%%%dd" % lnwidth code_block += nodes.inline(fstr[1:] % lineno, fstr[1:] % lineno, classes=['linenumber']) for line in content.split("\n"): if withln: code_block += nodes.inline(fstr % lineno, fstr % lineno, classes=['linenumber']) lineno += 1 code_block += nodes.Text(line + "\n", line + "\n") return [code_block] code_block_directive.arguments = (1, 0, 1) code_block_directive.content = 1 code_block_directive.options = {'include': directives.unchanged_required, 'linenos': directives.unchanged, 'linenos_offset': directives.unchanged, 'tab-width': directives.unchanged, } def rst_to_moinmoin(rstdata): # register code_block_directive with docutils directives.register_directive('code-block', code_block_directive) from docutils.core import publish_string return publish_string(rstdata, writer=MoinRSTWriter()) class WikiUpdater(object): def __init__(self): self.logger = logging.getLogger(self.__class__.__name__) self.rpcurl = "%s?action=xmlrpc2" % config.get('wiki', 'wikiurl') self.wikirpc = xmlrpclib.ServerProxy(self.rpcurl, allow_none=True) self.authtoken = None def _perform_auth(self): self.authtoken = self.wikirpc.getAuthToken( config.get('wiki', 'wikiuser'), config.get('wiki', 'wikipassword')) self.logger.info("authenticated to %s, got authentication token %s", self.rpcurl, self.authtoken) def updatePage(self, rstfile, dry_run=False): targetpage = config.get('pagemap', rstfile[:-len('.rst')]) self.logger.info("updating target page %s with content from %s", targetpage, rstfile) text = rst_to_moinmoin(open(rstfile, 'r').read()) if dry_run: self.logger.debug('dry run %s converted to\n%s', rstfile, text) return if not self.authtoken: self._perform_auth() mc = xmlrpclib.MultiCall(self.wikirpc) mc.applyAuthToken(self.authtoken) mc.putPage(targetpage, text) try: result = mc() for item in result: self.logger.info(item) except xmlrpclib.Fault, f: self.logger.warning("fault in XML-RPC call. code=%s, message=%s", f.faultCode, f.faultString) if __name__ == '__main__': if len(sys.argv) < 2: print "Usage: %s [-n] [ ...]" % sys.argv[0] sys.exit(1) wikiupdate = WikiUpdater() for filename in [filename for filename in sys.argv[1:] if filename != '-n']: wikiupdate.updatePage(filename, '-n' in sys.argv[1:])