common/rest.py
changeset 1808 aa09e20dd8c0
parent 1693 49075f57cf2c
parent 1807 6d541c610165
child 1810 e95e876be17c
equal deleted inserted replaced
1693:49075f57cf2c 1808:aa09e20dd8c0
     1 """rest publishing functions
       
     2 
       
     3 contains some functions and setup of docutils for cubicweb. Provides the
       
     4 following ReST directives:
       
     5 
       
     6 * `eid`, create link to entity in the repository by their eid
       
     7 
       
     8 * `card`, create link to card entity in the repository by their wikiid
       
     9   (proposing to create it when the refered card doesn't exist yet)
       
    10 
       
    11 * `winclude`, reference to a web documentation file (in wdoc/ directories)
       
    12 
       
    13 * `sourcecode` (if pygments is installed), source code colorization
       
    14 
       
    15 :organization: Logilab
       
    16 :copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
       
    17 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
       
    18 """
       
    19 __docformat__ = "restructuredtext en"
       
    20 
       
    21 from cStringIO import StringIO
       
    22 from itertools import chain
       
    23 from logging import getLogger
       
    24 from os.path import join
       
    25 
       
    26 from docutils import statemachine, nodes, utils, io
       
    27 from docutils.core import publish_string
       
    28 from docutils.parsers.rst import Parser, states, directives
       
    29 from docutils.parsers.rst.roles import register_canonical_role, set_classes
       
    30 
       
    31 from logilab.mtconverter import html_escape
       
    32 
       
    33 from cubicweb.common.html4zope import Writer
       
    34 
       
    35 # We provide our own parser as an attempt to get rid of
       
    36 # state machine reinstanciation
       
    37 
       
    38 import re
       
    39 # compile states.Body patterns
       
    40 for k, v in states.Body.patterns.items():
       
    41     if isinstance(v, str):
       
    42         states.Body.patterns[k] = re.compile(v)
       
    43 
       
    44 # register ReStructured Text mimetype / extensions
       
    45 import mimetypes
       
    46 mimetypes.add_type('text/rest', '.rest')
       
    47 mimetypes.add_type('text/rest', '.rst')
       
    48 
       
    49 
       
    50 LOGGER = getLogger('cubicweb.rest')
       
    51 
       
    52 def eid_reference_role(role, rawtext, text, lineno, inliner,
       
    53                        options={}, content=[]):
       
    54     try:
       
    55         try:
       
    56             eid_num, rest = text.split(u':', 1)
       
    57         except:
       
    58             eid_num, rest = text, '#'+text
       
    59         eid_num = int(eid_num)
       
    60         if eid_num < 0:
       
    61             raise ValueError
       
    62     except ValueError:
       
    63         msg = inliner.reporter.error(
       
    64             'EID number must be a positive number; "%s" is invalid.'
       
    65             % text, line=lineno)
       
    66         prb = inliner.problematic(rawtext, rawtext, msg)
       
    67         return [prb], [msg]
       
    68     # Base URL mainly used by inliner.pep_reference; so this is correct:
       
    69     context = inliner.document.settings.context
       
    70     refedentity = context.req.eid_rset(eid_num).get_entity(0, 0)
       
    71     ref = refedentity.absolute_url()
       
    72     set_classes(options)
       
    73     return [nodes.reference(rawtext, utils.unescape(rest), refuri=ref,
       
    74                             **options)], []
       
    75 
       
    76 register_canonical_role('eid', eid_reference_role)
       
    77 
       
    78 
       
    79 def card_reference_role(role, rawtext, text, lineno, inliner,
       
    80                        options={}, content=[]):
       
    81     text = text.strip()
       
    82     try:
       
    83         wikiid, rest = text.split(u':', 1)
       
    84     except:
       
    85         wikiid, rest = text, text
       
    86     context = inliner.document.settings.context
       
    87     cardrset = context.req.execute('Card X WHERE X wikiid %(id)s',
       
    88                                    {'id': wikiid})
       
    89     if cardrset:
       
    90         ref = cardrset.get_entity(0, 0).absolute_url()
       
    91     else:
       
    92         schema = context.schema
       
    93         if schema.eschema('Card').has_perm(context.req, 'add'):
       
    94             ref = context.req.build_url('view', vid='creation', etype='Card', wikiid=wikiid)
       
    95         else:
       
    96             ref = '#'
       
    97     set_classes(options)
       
    98     return [nodes.reference(rawtext, utils.unescape(rest), refuri=ref,
       
    99                             **options)], []
       
   100 
       
   101 register_canonical_role('card', card_reference_role)
       
   102 
       
   103 
       
   104 def winclude_directive(name, arguments, options, content, lineno,
       
   105                        content_offset, block_text, state, state_machine):
       
   106     """Include a reST file as part of the content of this reST file.
       
   107 
       
   108     same as standard include directive but using config.locate_doc_resource to
       
   109     get actual file to include.
       
   110 
       
   111     Most part of this implementation is copied from `include` directive defined
       
   112     in `docutils.parsers.rst.directives.misc`
       
   113     """
       
   114     context = state.document.settings.context
       
   115     source = state_machine.input_lines.source(
       
   116         lineno - state_machine.input_offset - 1)
       
   117     #source_dir = os.path.dirname(os.path.abspath(source))
       
   118     fid = arguments[0]
       
   119     for lang in chain((context.req.lang, context.vreg.property_value('ui.language')),
       
   120                       context.config.available_languages()):
       
   121         rid = '%s_%s.rst' % (fid, lang)
       
   122         resourcedir = context.config.locate_doc_file(rid)
       
   123         if resourcedir:
       
   124             break
       
   125     else:
       
   126         severe = state_machine.reporter.severe(
       
   127               'Problems with "%s" directive path:\nno resource matching %s.'
       
   128               % (name, fid),
       
   129               nodes.literal_block(block_text, block_text), line=lineno)
       
   130         return [severe]
       
   131     path = join(resourcedir, rid)
       
   132     encoding = options.get('encoding', state.document.settings.input_encoding)
       
   133     try:
       
   134         state.document.settings.record_dependencies.add(path)
       
   135         include_file = io.FileInput(
       
   136             source_path=path, encoding=encoding,
       
   137             error_handler=state.document.settings.input_encoding_error_handler,
       
   138             handle_io_errors=None)
       
   139     except IOError, error:
       
   140         severe = state_machine.reporter.severe(
       
   141               'Problems with "%s" directive path:\n%s: %s.'
       
   142               % (name, error.__class__.__name__, error),
       
   143               nodes.literal_block(block_text, block_text), line=lineno)
       
   144         return [severe]
       
   145     try:
       
   146         include_text = include_file.read()
       
   147     except UnicodeError, error:
       
   148         severe = state_machine.reporter.severe(
       
   149               'Problem with "%s" directive:\n%s: %s'
       
   150               % (name, error.__class__.__name__, error),
       
   151               nodes.literal_block(block_text, block_text), line=lineno)
       
   152         return [severe]
       
   153     if options.has_key('literal'):
       
   154         literal_block = nodes.literal_block(include_text, include_text,
       
   155                                             source=path)
       
   156         literal_block.line = 1
       
   157         return literal_block
       
   158     else:
       
   159         include_lines = statemachine.string2lines(include_text,
       
   160                                                   convert_whitespace=1)
       
   161         state_machine.insert_input(include_lines, path)
       
   162         return []
       
   163 
       
   164 winclude_directive.arguments = (1, 0, 1)
       
   165 winclude_directive.options = {'literal': directives.flag,
       
   166                               'encoding': directives.encoding}
       
   167 directives.register_directive('winclude', winclude_directive)
       
   168 
       
   169 try:
       
   170     from pygments import highlight
       
   171     from pygments.lexers import get_lexer_by_name, LEXERS
       
   172     from pygments.formatters import HtmlFormatter
       
   173 except ImportError:
       
   174     pass
       
   175 else:
       
   176     _PYGMENTS_FORMATTER = HtmlFormatter()
       
   177 
       
   178     def pygments_directive(name, arguments, options, content, lineno,
       
   179                            content_offset, block_text, state, state_machine):
       
   180         try:
       
   181             lexer = get_lexer_by_name(arguments[0])
       
   182         except ValueError:
       
   183             import traceback
       
   184             traceback.print_exc()
       
   185             print sorted(aliases for module_name, name, aliases, _, _  in LEXERS.itervalues())
       
   186             # no lexer found
       
   187             lexer = get_lexer_by_name('text')
       
   188         print 'LEXER', lexer
       
   189         parsed = highlight(u'\n'.join(content), lexer, _PYGMENTS_FORMATTER)
       
   190         context = state.document.settings.context
       
   191         context.req.add_css('pygments.css')
       
   192         return [nodes.raw('', parsed, format='html')]
       
   193      
       
   194     pygments_directive.arguments = (1, 0, 1)
       
   195     pygments_directive.content = 1
       
   196     directives.register_directive('sourcecode', pygments_directive)
       
   197 
       
   198 
       
   199 class CubicWebReSTParser(Parser):
       
   200     """The (customized) reStructuredText parser."""
       
   201 
       
   202     def __init__(self):
       
   203         self.initial_state = 'Body'
       
   204         self.state_classes = states.state_classes
       
   205         self.inliner = states.Inliner()
       
   206         self.statemachine = states.RSTStateMachine(
       
   207               state_classes=self.state_classes,
       
   208               initial_state=self.initial_state,
       
   209               debug=0)
       
   210 
       
   211     def parse(self, inputstring, document):
       
   212         """Parse `inputstring` and populate `document`, a document tree."""
       
   213         self.setup_parse(inputstring, document)
       
   214         inputlines = statemachine.string2lines(inputstring,
       
   215                                                convert_whitespace=1)
       
   216         self.statemachine.run(inputlines, document, inliner=self.inliner)
       
   217         self.finish_parse()
       
   218 
       
   219 
       
   220 _REST_PARSER = CubicWebReSTParser()
       
   221 
       
   222 def rest_publish(context, data):
       
   223     """publish a string formatted as ReStructured Text to HTML
       
   224     
       
   225     :type context: a cubicweb application object
       
   226 
       
   227     :type data: str
       
   228     :param data: some ReST text
       
   229 
       
   230     :rtype: unicode
       
   231     :return:
       
   232       the data formatted as HTML or the original data if an error occured
       
   233     """
       
   234     req = context.req
       
   235     if isinstance(data, unicode):
       
   236         encoding = 'unicode'
       
   237     else:
       
   238         encoding = req.encoding
       
   239     settings = {'input_encoding': encoding, 'output_encoding': 'unicode',
       
   240                 'warning_stream': StringIO(), 'context': context,
       
   241                 # dunno what's the max, severe is 4, and we never want a crash
       
   242                 # (though try/except may be a better option...)
       
   243                 'halt_level': 10, 
       
   244                 }
       
   245     if context:
       
   246         if hasattr(req, 'url'):
       
   247             base_url = req.url()
       
   248         elif hasattr(context, 'absolute_url'):
       
   249             base_url = context.absolute_url()
       
   250         else:
       
   251             base_url = req.base_url()
       
   252     else:
       
   253         base_url = None
       
   254     try:
       
   255         return publish_string(writer=Writer(base_url=base_url),
       
   256                               parser=_REST_PARSER, source=data,
       
   257                               settings_overrides=settings)
       
   258     except Exception:
       
   259         LOGGER.exception('error while publishing ReST text')
       
   260         if not isinstance(data, unicode):
       
   261             data = unicode(data, encoding, 'replace')
       
   262         return html_escape(req._('error while publishing ReST text')
       
   263                            + '\n\n' + data)