# -*- coding: utf-8 -*-
#
# This file is part of INGInious. See the LICENSE and the COPYRIGHTS files for
# more information about the licensing of this file.
""" Tools to parse text """
import html
import gettext
from datetime import datetime
import tidylib
from docutils import core, nodes
from docutils.parsers.rst import directives, Directive
from docutils.statemachine import StringList
from docutils.writers import html4css1
from inginious.frontend.accessible_time import parse_date
[docs]class HiddenUntilDirective(Directive, object):
required_arguments = 1
has_content = True
optional_arguments = 0
option_spec = {}
[docs] def run(self):
self.assert_has_content()
hidden_until = self.arguments[0]
try:
hidden_until = parse_date(hidden_until)
except:
raise self.error('Unknown date format in the "%s" directive; '
'%s' % (self.name, hidden_until))
force_show = self.state.document.settings.force_show_hidden_until
translation = self.state.document.settings.translation
after_deadline = hidden_until <= datetime.now()
if after_deadline or force_show:
output = []
# Add a warning for teachers/tutors/...
if not after_deadline and force_show:
node = nodes.caution()
self.add_name(node)
text = translation.gettext("The feedback below will be hidden to the students until {}.").format(hidden_until.strftime("%d/%m/%Y %H:%M:%S"))
self.state.nested_parse(StringList(text.split("\n")), 0, node)
output.append(node)
text = '\n'.join(self.content)
node = nodes.compound(text)
self.add_name(node)
self.state.nested_parse(self.content, self.content_offset, node)
output.append(node)
return output
else:
node = nodes.caution()
self.add_name(node)
text = translation.gettext("A part of this feedback is hidden until {}. Please come back later and reload the submission to see the full feedback.").format(
hidden_until.strftime("%d/%m/%Y %H:%M:%S"))
self.state.nested_parse(StringList(text.split("\n")), 0, node)
return [node]
directives.register_directive("hidden-until", HiddenUntilDirective)
class _CustomHTMLWriter(html4css1.Writer, object):
""" A custom HTML writer that fixes some defaults of docutils... """
def __init__(self):
html4css1.Writer.__init__(self)
self.translator_class = self._CustomHTMLTranslator
class _CustomHTMLTranslator(html4css1.HTMLTranslator, object): # pylint: disable=abstract-method
""" A custom HTML translator """
def visit_container(self, node):
""" Custom version of visit_container that do not put 'container' in div class"""
self.body.append(self.starttag(node, 'div'))
def visit_literal(self, node):
""" A custom version of visit_literal that uses the balise <code> instead of <tt>. """
# special case: "code" role
classes = node.get('classes', [])
if 'code' in classes:
# filter 'code' from class arguments
node['classes'] = [cls for cls in classes if cls != 'code']
self.body.append(self.starttag(node, 'code', ''))
return
self.body.append(
self.starttag(node, 'code', '', CLASS='docutils literal'))
text = node.astext()
for token in self.words_and_spaces.findall(text):
if token.strip():
# Protect text like "--an-option" and the regular expression
# ``[+]?(\d+(\.\d*)?|\.\d+)`` from bad line wrapping
if self.in_word_wrap_point.search(token):
self.body.append('<span class="pre">%s</span>'
% self.encode(token))
else:
self.body.append(self.encode(token))
elif token in ('\n', ' '):
# Allow breaks at whitespace:
self.body.append(token)
else:
# Protect runs of multiple spaces; the last space can wrap:
self.body.append(' ' * (len(token) - 1) + ' ')
self.body.append('</code>')
# Content already processed:
raise nodes.SkipNode
def starttag(self, node, tagname, suffix='\n', empty=False, **attributes):
""" Ensures all links to outside this instance of INGInious have target='_blank' """
if tagname == 'a' and "href" in attributes and not attributes["href"].startswith('#'):
attributes["target"] = "_blank"
return html4css1.HTMLTranslator.starttag(self, node, tagname, suffix, empty, **attributes)
[docs]class ParsableText(object):
"""Allow to parse a string with different parsers"""
def __init__(self, content, mode="rst", show_everything=False, translation=gettext.NullTranslations()):
"""
content The string to be parsed.
mode The parser to be used. Currently, only rst(reStructuredText) and HTML are supported.
show_everything Shows things that are normally hidden, such as the hidden-util directive.
"""
mode = mode.lower()
if mode not in ["rst", "html"]:
raise Exception("Unknown text parser: " + mode)
self._content = content
self._parsed = None
self._translation = translation
self._mode = mode
self._show_everything = show_everything
[docs] def original_content(self):
""" Returns the original content """
return self._content
[docs] def parse(self):
"""Returns parsed text"""
if self._parsed is None:
try:
if self._mode == "html":
self._parsed = self.html(self._content, self._show_everything, self._translation)
else:
self._parsed = self.rst(self._content, self._show_everything, self._translation)
except:
self._parsed = self._translation.gettext("<b>Parsing failed</b>: <pre>{}</pre>").format(html.escape(self._content))
return self._parsed
def __str__(self):
"""Returns parsed text"""
return self.parse()
def __unicode__(self):
"""Returns parsed text"""
return self.parse()
[docs] @classmethod
def html(cls, string, show_everything=False, translation=gettext.NullTranslations()): # pylint: disable=unused-argument
"""Parses HTML"""
out, _ = tidylib.tidy_fragment(string)
return out
[docs] @classmethod
def rst(cls, string, show_everything=False, translation=gettext.NullTranslations(), initial_header_level=3):
"""Parses reStructuredText"""
overrides = {
'initial_header_level': initial_header_level,
'doctitle_xform': False,
'syntax_highlight': 'none',
'force_show_hidden_until': show_everything,
'translation': translation,
'math_output': 'MathJax'
}
parts = core.publish_parts(source=string, writer=_CustomHTMLWriter(), settings_overrides=overrides)
return parts['body_pre_docinfo'] + parts['fragment']