Commit 72407b8e422af52f8c072536e4fc97b4bf918294
1 parent
325f383a
Exists in
master
and in
1 other branch
- using mistune to do markdown rendering, including highlighr and latex math.
- some corrections to demo
Showing
5 changed files
with
128 additions
and
254 deletions
Show diff stats
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.  | |
50 | + text: | | |
51 | + Texto negativo (alerta). Não conta para avaliação. | |
52 | + | |
53 | +  | |
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
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='', 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) | ... | ... |