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,14 +5,19 @@
5 text: | 5 text: |
6 Texto informativo. Não conta para avaliação. 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 ref: tut-success 16 ref: tut-success
12 type: success 17 type: success
13 title: success 18 title: success
14 text: | 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 ```C 22 ```C
18 int main() { 23 int main() {
@@ -42,7 +47,10 @@ @@ -42,7 +47,10 @@
42 ref: tut-alert 47 ref: tut-alert
43 type: alert 48 type: alert
44 title: alert 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,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 \ No newline at end of file 0 \ No newline at end of file
demo/test.yaml
@@ -1,74 +0,0 @@ @@ -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,7 +10,7 @@
10 <script type="text/x-mathjax-config"> 10 <script type="text/x-mathjax-config">
11 MathJax.Hub.Config({ 11 MathJax.Hub.Config({
12 tex2jax: { 12 tex2jax: {
13 - inlineMath: [["$$$","$$$"], ["$","$"], ["\(","\)"]] 13 + inlineMath: [["$$$","$$$"], ["\(","\)"]]
14 } 14 }
15 }); 15 });
16 </script> 16 </script>
1 1
  2 +# builtin
2 from os import path 3 from os import path
3 import subprocess 4 import subprocess
4 import logging 5 import logging
  6 +import re
5 7
  8 +# packages
6 import yaml 9 import yaml
7 -# import markdown  
8 import mistune 10 import mistune
  11 +# from markdown import markdown
9 from pygments import highlight 12 from pygments import highlight
10 from pygments.lexers import get_lexer_by_name 13 from pygments.lexers import get_lexer_by_name
11 -from pygments.formatters import html 14 +from pygments.formatters import HtmlFormatter
12 15
13 # setup logger for this module 16 # setup logger for this module
14 logger = logging.getLogger(__name__) 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 class HighlightRenderer(mistune.Renderer): 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 lexer = get_lexer_by_name(lang, stripall=True) 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 def image(self, src, title, text): 121 def image(self, src, title, text):
28 src = 'FIXME' # FIXME 122 src = 'FIXME' # FIXME
29 return super().image(src, title, text) 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 # load data from yaml file 142 # load data from yaml file
37 # --------------------------------------------------------------------------- 143 # ---------------------------------------------------------------------------
@@ -79,7 +185,3 @@ def run_script(script, stdin=&#39;&#39;, timeout=5): @@ -79,7 +185,3 @@ def run_script(script, stdin=&#39;&#39;, timeout=5):
79 logger.error('Error parsing yaml output of "{script}"') 185 logger.error('Error parsing yaml output of "{script}"')
80 else: 186 else:
81 return output 187 return output
82 -  
83 -# ---------------------------------------------------------------------------  
84 -def md_to_html(text, q=None):  
85 - return markdown(text)