Add ReST support to web front end
authorAaron Bentley <abentley@panoramicfeedback.com>
Thu, 6 Apr 2006 18:05:47 +0000 (14:05 -0400)
committerAaron Bentley <abentley@panoramicfeedback.com>
Thu, 6 Apr 2006 18:05:47 +0000 (14:05 -0400)
.be/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4/comments/2bb9163c-a2c4-4301-aff5-385f58a14301/body [new file with mode: 0644]
.be/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4/comments/2bb9163c-a2c4-4301-aff5-385f58a14301/values [new file with mode: 0644]
.be/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4/values [new file with mode: 0644]
beweb/beweb/controllers.py
beweb/beweb/formatting.py [new file with mode: 0644]
beweb/beweb/templates/edit_bug.kid
libbe/restconvert.py [new file with mode: 0644]

diff --git a/.be/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4/comments/2bb9163c-a2c4-4301-aff5-385f58a14301/body b/.be/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4/comments/2bb9163c-a2c4-4301-aff5-385f58a14301/body
new file mode 100644 (file)
index 0000000..21fb43d
--- /dev/null
@@ -0,0 +1 @@
+Add *support*, damnit!
diff --git a/.be/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4/comments/2bb9163c-a2c4-4301-aff5-385f58a14301/values b/.be/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4/comments/2bb9163c-a2c4-4301-aff5-385f58a14301/values
new file mode 100644 (file)
index 0000000..58e8ffa
--- /dev/null
@@ -0,0 +1,21 @@
+
+
+
+Content-type=text/rst
+
+
+
+
+
+
+Date=Thu, 06 Apr 2006 16:47:25 +0000
+
+
+
+
+
+
+From=abentley
+
+
+
diff --git a/.be/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4/values b/.be/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4/values
new file mode 100644 (file)
index 0000000..9f3c7ac
--- /dev/null
@@ -0,0 +1,35 @@
+
+
+
+creator=abentley
+
+
+
+
+
+
+severity=minor
+
+
+
+
+
+
+status=closed
+
+
+
+
+
+
+summary=Support RST
+
+
+
+
+
+
+time=Thu, 06 Apr 2006 16:45:52 +0000
+
+
+
index 74c6a7da89f84ec997e2b2b403c43cccf8d55d39..fc505ed9c2771443f55755eba8b7543528af61f7 100644 (file)
@@ -23,6 +23,7 @@ class Comment(PrestHandler):
         bug_tree = project_tree(comment_data['project'])
         bug = bug_tree.get_bug(comment_data['bug'])
         comment = new_comment(bug, "")
+        comment.content_type = "text/restructured"
         comment.save()
         raise cherrypy.HTTPRedirect(comment_url(comment=comment.uuid, 
                                     **comment_data))
diff --git a/beweb/beweb/formatting.py b/beweb/beweb/formatting.py
new file mode 100644 (file)
index 0000000..44ed849
--- /dev/null
@@ -0,0 +1,73 @@
+from StringIO import StringIO
+
+from elementtree.ElementTree import XML
+from libbe.restconvert import rest_xml
+
+def to_unix(text):
+   skip_newline = False
+   for ch in text:
+      if ch not in ('\r', '\n'):
+         yield ch
+      else:
+         if ch == '\n':
+            if skip_newline:
+               continue
+         else:
+            skip_newline = True
+         yield '\n'
+
+
+def soft_text(text):
+   first_space = False
+   translations = {'\n': '<br />\n', '&': '&amp;', '\x3c': '&lt;', 
+                   '\x3e': '&gt;'}
+   for ch in to_unix(text):
+      if ch == ' ' and first_space is True:
+            yield '&#160;'
+      first_space = ch in (' ')
+      try:
+         yield translations[ch]
+      except KeyError:
+         yield ch
+
+
+def soft_pre(text):
+   return XML('<div style="font-family: monospace">'+
+              ''.join(soft_text(text))+'</div>') 
+
+
+def get_rest_body(rest):
+    xml, warnings = rest_xml(StringIO(rest))
+    return xml.find('{http://www.w3.org/1999/xhtml}body'), warnings
+
+def comment_body_xhtml(comment):
+    if comment.content_type == "text/restructured":
+        return get_rest_body(comment.body)[0]
+    else:
+        return soft_pre(comment.body)
+
+
+def select_among(name, options, default, display_names=None):
+    output = ['<select name="%s">' % name]
+    for option in options:
+        if option == default:
+            selected = ' selected="selected"'
+        else:
+            selected = ""
+        if display_names is None:
+            display_name = None
+        else:
+            display_name = display_names.get(option)
+
+        if option is None:
+            option = ""
+        if display_name is None:
+            display_name = option
+            value = ""
+        else:
+            value = ' value="%s"' % option
+        output.append("<option%s%s>%s</option>" % (selected, value, 
+                                                   display_name))
+    output.append("</select>")
+    return XML("".join(output))
index 960866d38e876a66a862f62a7d92dd14b938c7de..c31d6601397ea14a2d6f495ca32fbd503138e42d 100644 (file)
@@ -4,58 +4,7 @@ from libbe.bugdir import severity_levels, active_status, inactive_status, thread
 from libbe.utility import time_to_str 
 from beweb.controllers import bug_list_url, comment_url
 from beweb.config import people
-def select_among(name, options, default, display_names=None):
-    output = ['<select name="%s">' % name]
-    for option in options:
-        if option == default:
-            selected = ' selected="selected"'
-        else:
-            selected = ""
-        if display_names is None:
-            display_name = None
-        else:
-            display_name = display_names.get(option)
-
-        if option is None:
-            option = ""
-        if display_name is None:
-            display_name = option
-            value = ""
-        else:
-            value = ' value="%s"' % option
-        output.append("<option%s%s>%s</option>" % (selected, value, 
-                                                   display_name))
-    output.append("</select>")
-    return XML("".join(output))
-
-def to_unix(text):
-   skip_newline = False
-   for ch in text:
-      if ch not in ('\r', '\n'):
-         yield ch
-      else:
-         if ch == '\n':
-            if skip_newline:
-               continue
-         else:
-            skip_newline = True
-         yield '\n'
-
-def soft_text(text):
-   first_space = False
-   translations = {'\n': '<br />\n', '&': '&amp;', '\x3c': '&lt;', 
-                   '\x3e': '&gt;'}
-   for ch in to_unix(text):
-      if ch == ' ' and first_space is True:
-            yield '&#160;'
-      first_space = ch in (' ')
-      try:
-         yield translations[ch]
-      except KeyError:
-         yield ch
-def soft_pre(text):
-   return XML('<div style="font-family: monospace">'+
-              ''.join(soft_text(text))+'</div>') 
+from beweb.formatting import comment_body_xhtml, select_among
 ?>
 <html xmlns="http://www.w3.org/1999/xhtml" xmlns:py="http://purl.org/kid/ns#"
     py:extends="'master.kid'">
@@ -79,7 +28,7 @@ def soft_pre(text):
         <tr><td>From</td><td>${comment.From}</td></tr>
         <tr><td>Date</td><td>${time_to_str(comment.date)}</td></tr>
     </table>
-    <div py:content="soft_pre(comment.body)" py:strip="True"></div>
+    <div py:content="comment_body_xhtml(comment)" py:strip="True"></div>
     <a href="${comment_url(project_id, bug.uuid, comment.uuid)}">Edit</a>
     <a href="${comment_url(project_id, bug.uuid, comment.uuid, 
                            action='Reply')}">Reply</a>
diff --git a/libbe/restconvert.py b/libbe/restconvert.py
new file mode 100644 (file)
index 0000000..f93fcf6
--- /dev/null
@@ -0,0 +1,108 @@
+import re
+from StringIO import StringIO
+from docutils import nodes
+from docutils.statemachine import StringList
+from docutils.core import publish_file
+from docutils.parsers import rst
+from docutils.parsers.rst import directives
+from docutils.parsers.rst.states import Inliner, MarkupMismatch, unescape
+from elementtree import ElementTree
+
+
+def rest_xml(rest):
+    warnings = StringIO()
+    parser = rst.Parser(inliner=HelpLinkInliner())
+    xmltext = publish_file(rest, writer_name="html", parser=parser,
+                           settings_overrides={"warning_stream": warnings,
+                                               "halt_level": 5})
+    warnings.seek(0)
+    return ElementTree.parse(StringIO(xmltext)).getroot(), warnings.read()
+
+class HelpLinkInliner(Inliner):
+    def __init__(self, roles=None):
+        Inliner.__init__(self, roles)
+        regex = re.compile('\[([^|]*)\|([^]]*)\]')
+        self.implicit_dispatch.append((regex, self.help_reference))
+
+    def parse(self, *args, **kwargs):
+        self.more_messages = []
+        nodes, messages = Inliner.parse(self, *args, **kwargs)
+        return nodes, (messages + self.more_messages)
+        
+    def help_reference(self, match, lineno):
+        from wizardhelp.controllers import iter_help_pages
+        text,link = match.groups()
+        rawtext = match.group(0)
+        text, link, rawtext = [unescape(f, 1) for f in (text, link, rawtext)]
+        if link not in list(iter_help_pages()):
+            msg = self.reporter.warning('Broken link to "%s".' % link, 
+                                        line=lineno)
+            self.more_messages.append(msg)
+        ref = "/help/%s/" % link
+        unescaped = text
+        node = nodes.reference(rawtext, text, refuri=ref)
+        node.set_class("helplink")
+        return [node]
+
+
+def rst_directive(name=None, required_args=0, optional_args=0, 
+                  final_arg_ws=False, options=None, content='forbidden'):
+    """Decorator that simplifies creating ReST directives
+    
+    All arguments are optional.  Name is, by default, determined from the
+    function name.
+
+    The possible values for content are 'forbidden', 'allowed' (but not 
+    required), and 'required' (a warning will be generated if not present).
+    """
+    content_rules = {'forbidden': (False, False), 'allowed': (True, False), 
+                     'required': (True, True)}
+    content_allowed, content_required = content_rules[content]
+
+    def decorator_factory(func):
+        my_name = name
+        if my_name is None:
+            my_name = func.__name__
+
+        def decorator(name, arguments, options, content, lineno, 
+                      content_offset, block_text, state, state_machine):
+            warn = state_machine.reporter.warning
+            if not content and content_required:
+                warn = state_machine.reporter.warning
+                warning = warn('%s is empty' % my_name,
+                               nodes.literal_block(block_text, block_text),
+                               line=lineno)
+                return [warning]
+            return func(name, arguments, options, content, lineno,
+                        content_offset, block_text, state, state_machine)
+
+        decorator.arguments = (required_args, optional_args, final_arg_ws)
+        decorator.options = options
+        decorator.content = content_allowed
+        directives.register_directive(my_name, decorator)
+        return decorator 
+    return decorator_factory
+
+
+@rst_directive(required_args=1, final_arg_ws=True, content='required')
+def foldout(name, arguments, options, content, lineno, content_offset, 
+            block_text, state, state_machine):
+    """\
+    Generate a foldout section.
+    
+    On the ReST side, this merely involves marking the items with suitable
+    classes.  A Kid match rule will be used to insert the appropriate
+    Javascript magic.
+    """
+    text = '\n'.join(content)
+    foldout_title = nodes.paragraph([arguments[0]])
+    foldout_title.set_class('foldout-title')
+    state.nested_parse(StringList([arguments[0]]), 0, foldout_title)
+    foldout_body = nodes.compound(text)
+    foldout_body.set_class('foldout-body')
+    state.nested_parse(content, content_offset, foldout_body)
+    foldout = nodes.compound(text)
+    foldout += foldout_title
+    foldout += foldout_body
+    foldout.set_class('foldout')
+    return [foldout]