Commit 72407b8e422af52f8c072536e4fc97b4bf918294

Authored by Miguel Barao
1 parent 325f383a
Exists in master and in 1 other branch dev

- using mistune to do markdown rendering, including highlighr and latex math.

- some corrections to demo
demo/questions/questions-tutorial.yaml
... ... @@ -5,14 +5,19 @@
5 5 text: |
6 6 Texto informativo. Não conta para avaliação.
7 7  
8   - $$ p(x) = \frac{1}{\sqrt{2\pi\sigma^2}}e^{-\frac{(x-\mu)^2}{2\sigma^2}} $$
  8 + A Distribuição gaussiana $\mathcal{N}(x\mid\mu,\sigma^2)$ é definida por
  9 +
  10 + $$
  11 + p(x) = \frac{1}{\sqrt{2\pi\sigma^2}}e^{-\frac{(x-\mu)^2}{2\sigma^2}}.
  12 + $$
  13 +
9 14 # ---------------------------------------------------------------------------
10 15 -
11 16 ref: tut-success
12 17 type: success
13 18 title: success
14 19 text: |
15   - Texto de positivo (sucesso). Não conta para avaliação.
  20 + Texto positivo (sucesso). Não conta para avaliação.
16 21  
17 22 ```C
18 23 int main() {
... ... @@ -42,7 +47,10 @@
42 47 ref: tut-alert
43 48 type: alert
44 49 title: alert
45   - text: Texto negativo (alerta). Não conta para avaliação. ![alt text](abc.jpg "Logo Title Text 1")
  50 + text: |
  51 + Texto negativo (alerta). Não conta para avaliação.
  52 +
  53 + ![imagem](image.jpg "Título da imagem")
46 54  
47 55 # ----------------------------------------------------------------------------
48 56 -
... ...
demo/questions/questions.yaml
... ... @@ -1,162 +0,0 @@
1   --
2   - ref: flags
3   - type: radio
4   - files:
5   - flag1: images/flag-pt.svg
6   - flag2: images/flag-es.svg
7   - flag3: images/flag-fr.svg
8   - text: Qual é a bandeira de Portugal?
9   - options:
10   - - '<img src="flag1" width="200" alt="vermelha e verde" />'
11   - - '<img src="flag2" width="200" alt="vermelha e amarela" />'
12   - - '<img src="flag3" width="200" alt="azul, branca e vermelha" />'
13   - # opcional
14   - title: Bandeiras nacionais
15   - hint: |
16   - > A Portuguesa, que hoje é um dos símbolos nacionais de Portugal (o seu hino nacional), nasceu como uma canção de cariz patriótico em resposta ao ultimato britânico para que as tropas portuguesas abandonassem as suas posições em África, no denominado "Mapa cor-de-rosa".
17   - >
18   - > -- <cite>da Wikipedia</cite>
19   -
20   - <a href="https://pt.wikipedia.org/wiki/A_Portuguesa">Mais informação</a>
21   -
22   - <iframe width="560" height="315" src="https://www.youtube.com/embed/DdOEpfypWQA" frameborder="0" allowfullscreen></iframe>
23   -# ---------------------------------------------------------------------------
24   --
25   - ref: solar-system
26   - type: radio
27   - files:
28   - solar_system_planets: images/planets.png
29   - text: Qual é o maior planeta do Sistema Solar? <img src="solar_system_planets" width="100%"/>
30   - options:
31   - - Mercúrio
32   - - Marte
33   - - Júpiter
34   - - Têm todos o mesmo tamanho
35   - # opcional
36   - title: Sistema solar
37   - correct: 2
38   - shuffle: False
39   - discount: True
40   - hint: Se usar o markdown `!(text)[imagem]` a imagem fica com tamanho fixo. É preferível usar a tag html `<img with="100%" src="...">` para ter sempre a largura correcta.
41   -# ---------------------------------------------------------------------------
42   --
43   - ref: math-expressions
44   - type: checkbox
45   - text: Quais das seguintes expressões são verdadeiras?
46   - options:
47   - - $1 > 0$
48   - - $\sqrt{3} > \sqrt{2}$
49   - - $e^{i\pi} + 1 = 0$
50   - - $\frac{\partial f(x,y)}{\partial z} = 1$
51   - - $-1 > 1$
52   - correct: [1, 1, 1, -1, -1]
53   - # optional
54   - title: Expressões matemáticas
55   - shuffle: True
56   - discount: True
57   - hint: Duas delas são falsas.
58   -# ---------------------------------------------------------------------------
59   --
60   - ref: our_planet1
61   - type: text
62   - text: O nosso planeta chama-se planeta...
63   - correct: ['Terra', 'terra']
64   -# ---------------------------------------------------------------------------
65   --
66   - ref: our_planet2
67   - type: text-regex
68   - text: O nosso planeta chama-se planeta...
69   - correct: !regex '[Tt]erra'
70   - # ---------------------------------------------------------------------------
71   --
72   - ref: fractions
73   - type: numeric-interval
74   - text: Quanto é 1/4?
75   - correct: [0.249, 0.251]
76   -# ---------------------------------------------------------------------------
77   --
78   - ref: basic-colors
79   - type: textarea
80   - text: Escreva o nome das três cores básicas em inglês.
81   - correct: correct/correct-question.py
82   - # opcional
83   - lines: 3
84   - timeout: 5
85   - hint: Qualquer ordem serve.
86   -# ---------------------------------------------------------------------------
87   --
88   - ref: question-whatever
89   - type: generator
90   - script: generators/generate-question.py
91   - # opcional
92   - arg: "11,120"
93   - # the script should print a question in yaml format.
94   - # Print only the dictionary, not the list of dictionaries like here.
95   -# ---------------------------------------------------------------------------
96   --
97   - ref: instructions
98   - type: alert
99   - title: Atenção
100   - text: |
101   - Deverá indicar em cada pergunta se pretende ou não classificá-la. Se não quiser responder a uma questão, desactive-a para evitar penalizações na nota final.
102   -
103   -# ---------------------------------------------------------------------------
104   --
105   - ref: markdown_instructions
106   - type: information
107   - title: Que mais se pode incluir nas perguntas?
108   -
109   - # allow these files will be served:
110   - files:
111   - imagem_privada: images/flag-pt.svg
112   -
113   - text: |
114   - O texto das perguntas é escrito em __markdown__ e pode incluir algumas *tags* __html__. É usado __MathJax__ para gerar fórmulas matemáticas com a *syntax* do __LaTeX__.
115   -
116   - ### LaTeX
117   -
118   - As soluções da equação $ax^2+bx+c=0$ são obtidas com a fórmula resolvente:
119   - $$
120   - x = \frac{-b\pm\sqrt{b^2-4ac}}{2a}
121   - $$
122   -
123   - ### Código
124   -
125   - Suporta *syntax highlight* em múltiplas linguagens.
126   -
127   - C:
128   - ```C
129   - int main() {
130   - printf("Hello world!");
131   - return 0; // comentario
132   - }
133   - ```
134   -
135   - Python:
136   - ```python
137   - def myfunc(x, y):
138   - 'returna soma dos argumentos'
139   - return x + y + 1.0 # mais uma unidade
140   - ```
141   -
142   - Java:
143   - ```java
144   - public class HelloWorld {
145   - public static void main(String[] args) {
146   - // tanta coisa para dizer ola...
147   - System.out.println("Hello, World");
148   - }
149   - }
150   - ```
151   -
152   - ### Tabelas
153   -
154   - Left | Center | Right
155   - -----------------|:-------------:|----------:
156   - $\sin(x^2)$ | *hello* | $1600.00
157   - $\frac{1}{2\pi}$ | **world** | $12.50
158   - $\sqrt{\pi}$ | `code` | $1.99
159   -
160   - ---
161   -
162   - (c) By the author
163 0 \ No newline at end of file
demo/test.yaml
... ... @@ -1,74 +0,0 @@
1   -#=============================================================================
2   -# The test reference should be a unique identifier. It is saved in the database
3   -# so that queries for the results can be done in the terminal with
4   -# $ sqlite3 students.db "select * from tests where ref='demo'"
5   -ref: demo
6   -
7   -# (optional, default: '') You may wish to refer the course, year or kind of test
8   -title: Teste de Demonstração
9   -
10   -# (optional) duration in minutes FIXME
11   -duration: 90
12   -
13   -# Database with student credentials and grades of all questions and tests done
14   -# The database is an sqlite3 file generate with the script initdb.py
15   -database: demo/students.db
16   -
17   -# Generate a file for each test done by a student.
18   -# It includes the questions, answers and grades.
19   -answers_dir: demo/ans
20   -
21   -# (optional, default: False) Show points for each question, scale 0-20.
22   -show_points: True
23   -
24   -# (optional, default: False) Show hints if available
25   -show_hints: True
26   -
27   -# (optional, default: False) Show lots of information for debugging
28   -# debug: True
29   -
30   -#-----------------------------------------------------------------------------
31   -# Base path applied to the questions files and all the scripts
32   -# including question generators and correctors.
33   -# Either absolute path or relative to current directory can be used.
34   -questions_dir: demo/questions
35   -
36   -# (optional) List of files containing questions in yaml format.
37   -# Selected questions will be obtained from these files.
38   -# If undefined, all yaml files in questions_dir are loaded (not recommended).
39   -files:
40   - - questions.yaml
41   -
42   -# This is the list of questions that will make up the test.
43   -# The order is preserved.
44   -# There are several ways to define each question (explained below).
45   -questions:
46   - # show question where ref=instructions
47   - # - ref: instructions
48   -
49   - # show question where ref=flags and assigns 0.5 points (unnormalized)
50   - - ref: flags
51   - points: 0.5
52   -
53   - # idem
54   - - ref: math-expressions
55   - points: 2.0
56   -
57   - # # show question where ref=solar-system and assign the default of 1.0 point (unnormalized)
58   - - ref: solar-system
59   -
60   - # # select one questions from the list [our_planet1, our_planet2]
61   - # # and assign 0.75 points (unnormalized)
62   - - ref:
63   - - our_planet1
64   - - our_planet2
65   - points: 0.75
66   -
67   - # # the key 'ref:' can be omitted, a default of 1.0 points is assigned
68   - - basic-colors
69   -
70   - - fractions
71   -
72   - - question-whatever
73   -
74   - - markdown_instructions
templates/test.html
... ... @@ -10,7 +10,7 @@
10 10 <script type="text/x-mathjax-config">
11 11 MathJax.Hub.Config({
12 12 tex2jax: {
13   - inlineMath: [["$$$","$$$"], ["$","$"], ["\(","\)"]]
  13 + inlineMath: [["$$$","$$$"], ["\(","\)"]]
14 14 }
15 15 });
16 16 </script>
... ...
tools.py
1 1  
  2 +# builtin
2 3 from os import path
3 4 import subprocess
4 5 import logging
  6 +import re
5 7  
  8 +# packages
6 9 import yaml
7   -# import markdown
8 10 import mistune
  11 +# from markdown import markdown
9 12 from pygments import highlight
10 13 from pygments.lexers import get_lexer_by_name
11   -from pygments.formatters import html
  14 +from pygments.formatters import HtmlFormatter
12 15  
13 16 # setup logger for this module
14 17 logger = logging.getLogger(__name__)
15 18  
16 19  
17 20 # ---------------------------------------------------------------------------
  21 +# Markdown to HTML renderer with support for LaTeX equations
  22 +# Inline math: $x$
  23 +# Block math: $$x$$ or \begin{equation}x\end{equation}
  24 +# ---------------------------------------------------------------------------
  25 +class MathBlockGrammar(mistune.BlockGrammar):
  26 + block_math = re.compile(r"^\$\$(.*?)\$\$", re.DOTALL)
  27 + latex_environment = re.compile(r"^\\begin\{([a-z]*\*?)\}(.*?)\\end\{\1\}", re.DOTALL)
  28 +
  29 +
  30 +class MathBlockLexer(mistune.BlockLexer):
  31 + default_rules = ['block_math', 'latex_environment'] + mistune.BlockLexer.default_rules
  32 +
  33 + def __init__(self, rules=None, **kwargs):
  34 + if rules is None:
  35 + rules = MathBlockGrammar()
  36 + super().__init__(rules, **kwargs)
  37 +
  38 + def parse_block_math(self, m):
  39 + """Parse a $$math$$ block"""
  40 + self.tokens.append({
  41 + 'type': 'block_math',
  42 + 'text': m.group(1)
  43 + })
  44 +
  45 + def parse_latex_environment(self, m):
  46 + self.tokens.append({
  47 + 'type': 'latex_environment',
  48 + 'name': m.group(1),
  49 + 'text': m.group(2)
  50 + })
  51 +
  52 +
  53 +class MathInlineGrammar(mistune.InlineGrammar):
  54 + math = re.compile(r"^\$(.+?)\$", re.DOTALL)
  55 + block_math = re.compile(r"^\$\$(.+?)\$\$", re.DOTALL)
  56 + text = re.compile(r'^[\s\S]+?(?=[\\<!\[_*`~$]|https?://| {2,}\n|$)')
  57 +
  58 +
  59 +class MathInlineLexer(mistune.InlineLexer):
  60 + default_rules = ['block_math', 'math'] + mistune.InlineLexer.default_rules
  61 +
  62 + def __init__(self, renderer, rules=None, **kwargs):
  63 + if rules is None:
  64 + rules = MathInlineGrammar()
  65 + super().__init__(renderer, rules, **kwargs)
  66 +
  67 + def output_math(self, m):
  68 + return self.renderer.inline_math(m.group(1))
  69 +
  70 + def output_block_math(self, m):
  71 + return self.renderer.block_math(m.group(1))
  72 +
  73 +
  74 +class MarkdownWithMath(mistune.Markdown):
  75 + def __init__(self, renderer, **kwargs):
  76 + if 'inline' not in kwargs:
  77 + kwargs['inline'] = MathInlineLexer
  78 + if 'block' not in kwargs:
  79 + kwargs['block'] = MathBlockLexer
  80 + super().__init__(renderer, **kwargs)
  81 +
  82 + def output_block_math(self):
  83 + return self.renderer.block_math(self.token['text'])
  84 +
  85 + def output_latex_environment(self):
  86 + return self.renderer.latex_environment(self.token['name'], self.token['text'])
  87 +
  88 +
  89 +
18 90 class HighlightRenderer(mistune.Renderer):
19   - def block_code(self, code, lang=None):
20   - if lang is None:
21   - return f'\n<pre><code>{mistune.escape(code)}</code></pre>\n'
22   - else:
  91 + def block_code(self, code, lang='text'):
  92 + try:
23 93 lexer = get_lexer_by_name(lang, stripall=True)
24   - formatter = html.HtmlFormatter()
25   - return highlight(code, lexer, formatter)
  94 + except:
  95 + lexer = get_lexer_by_name('text', stripall=True)
  96 +
  97 + formatter = HtmlFormatter()
  98 + return "{open_block}{formatted}{close_block}".format(
  99 + open_block="<div class='code-highlight'>" if lang != 'text' else '',
  100 + formatted=highlight(code, lexer, formatter),
  101 + close_block="</div>" if lang != 'text' else ''
  102 + )
  103 +
  104 + # def table(self, header, body):
  105 + # return "<table class='table table-bordered table-hover'>" + header + body + "</table>"
  106 +
  107 + # def image(self, src, title, text):
  108 + # if src.startswith('javascript:'):
  109 + # src = ''
  110 + # text = mistune.escape(text, quote=True)
  111 + # if title:
  112 + # title = mistune.escape(title, quote=True)
  113 + # html = '<img class="img-responsive center-block" src="%s" alt="%s" title="%s"' % (src, text, title)
  114 + # else:
  115 + # html = '<img class="img-responsive center-block" src="%s" alt="%s"' % (src, text)
  116 + # if self.options.get('use_xhtml'):
  117 + # return '%s />' % html
  118 + # return '%s>' % html
  119 +
26 120  
27 121 def image(self, src, title, text):
28 122 src = 'FIXME' # FIXME
29 123 return super().image(src, title, text)
30 124  
31   -renderer = HighlightRenderer(hard_wrap=True)
32   -markdown = mistune.Markdown(renderer=renderer)
  125 + # Pass math through unaltered - mathjax does the rendering in the browser
  126 + def block_math(self, text):
  127 + return fr'\[ {text} \]'
  128 +
  129 + def latex_environment(self, name, text):
  130 + return fr'\begin{{{name}}} {text} \end{{{name}}}'
  131 +
  132 + def inline_math(self, text):
  133 + return fr'\( {text} \)'
33 134  
34 135  
  136 +markdown = MarkdownWithMath(HighlightRenderer(escape=False)) # hard_wrap=True to insert <br> on newline
  137 +
  138 +def md_to_html(text, q=None):
  139 + return markdown(text)
  140 +
35 141 # ---------------------------------------------------------------------------
36 142 # load data from yaml file
37 143 # ---------------------------------------------------------------------------
... ... @@ -79,7 +185,3 @@ def run_script(script, stdin=&#39;&#39;, timeout=5):
79 185 logger.error('Error parsing yaml output of "{script}"')
80 186 else:
81 187 return output
82   -
83   -# ---------------------------------------------------------------------------
84   -def md_to_html(text, q=None):
85   - return markdown(text)
... ...