Commit 939be670c9e323d2967031b2ea4ba56c32c4d729

Authored by Miguel Barão
1 parent cad28d33
Exists in dev

update jquery. move links to include-libs.html

aprendizations/__init__.py
1   -# Copyright (C) 2019 Miguel Barão
  1 +# Copyright © 2022 Miguel Barão
2 2 #
3 3 # THE MIT License
4 4 #
... ... @@ -30,10 +30,10 @@ are progressively uncovered as the students progress.
30 30 '''
31 31  
32 32 APP_NAME = 'aprendizations'
33   -APP_VERSION = '2022.08.dev1'
  33 +APP_VERSION = '2022.12.dev1'
34 34 APP_DESCRIPTION = __doc__
35 35  
36 36 __author__ = 'Miguel Barão'
37   -__copyright__ = 'Copyright 2021, Miguel Barão'
  37 +__copyright__ = 'Copyright © 2022, Miguel Barão'
38 38 __license__ = 'MIT license'
39 39 __version__ = APP_VERSION
... ...
aprendizations/serve.py
... ... @@ -11,6 +11,7 @@ import mimetypes
11 11 from os.path import join, dirname, expanduser
12 12 import signal
13 13 import sys
  14 +import re
14 15 from typing import List, Optional, Union
15 16 import uuid
16 17  
... ... @@ -21,7 +22,7 @@ import tornado.web
21 22 from tornado.escape import to_unicode
22 23  
23 24 # this project
24   -from aprendizations.tools import md_to_html
  25 +from aprendizations.renderer_markdown import md_to_html
25 26 from aprendizations.learnapp import LearnException
26 27 from aprendizations import APP_NAME
27 28  
... ... @@ -82,6 +83,7 @@ class BaseHandler(tornado.web.RequestHandler):
82 83 '''
83 84 Base handler common to all handlers.
84 85 '''
  86 +
85 87 @property
86 88 def learn(self):
87 89 '''easier access to learnapp'''
... ... @@ -105,6 +107,7 @@ class RankingsHandler(BaseHandler):
105 107 '''
106 108 Handles rankings page
107 109 '''
  110 +
108 111 @tornado.web.authenticated
109 112 def get(self) -> None:
110 113 '''
... ... @@ -130,33 +133,30 @@ class LoginHandler(BaseHandler):
130 133 '''
131 134 Handles /login
132 135 '''
  136 +
133 137 def get(self) -> None:
134 138 '''
135   - Renders login page
  139 + Render login page
136 140 '''
137   - self.render('login.html',
138   - appname=APP_NAME,
139   - error='')
  141 + self.render('login.html', appname=APP_NAME, error='')
140 142  
141 143 async def post(self):
142 144 '''
143   - Perform authentication and redirects to application if successful
  145 + Authenticate and redirect to application if successful
144 146 '''
145   -
146   - userid = (self.get_body_argument('uid') or '').lstrip('l')
  147 + userid = self.get_body_argument('uid') or ''
147 148 passwd = self.get_body_argument('pw')
148   -
149   - login_ok = await self.learn.login(userid, passwd)
150   -
151   - if login_ok:
152   - counter = str(self.learn.get_login_counter(userid))
153   - self.set_secure_cookie('aprendizations_user', userid)
154   - self.set_secure_cookie('counter', counter)
155   - self.redirect('/')
156   - else:
157   - self.render('login.html',
158   - appname=APP_NAME,
159   - error='Número ou senha incorrectos')
  149 + match = re.search(r'[0-9]+', userid) # extract number
  150 + if match is not None:
  151 + userid = match.group(0) # get string with number
  152 + if await self.learn.login(userid, passwd):
  153 + counter = str(self.learn.get_login_counter(userid))
  154 + self.set_secure_cookie('aprendizations_user', userid)
  155 + self.set_secure_cookie('counter', counter)
  156 + self.redirect('/')
  157 + self.render('login.html',
  158 + appname=APP_NAME,
  159 + error='Número ou senha incorrectos')
160 160  
161 161  
162 162 # ----------------------------------------------------------------------------
... ... @@ -167,7 +167,7 @@ class LogoutHandler(BaseHandler):
167 167 @tornado.web.authenticated
168 168 def get(self) -> None:
169 169 '''
170   - clears cookies and removes user session
  170 + clear cookies and user session
171 171 '''
172 172 self.clear_cookie('user')
173 173 self.clear_cookie('counter')
... ... @@ -182,14 +182,14 @@ class ChangePasswordHandler(BaseHandler):
182 182 '''
183 183 Handles password change
184 184 '''
  185 +
185 186 @tornado.web.authenticated
186 187 async def post(self) -> None:
187 188 '''
188   - Tries to perform password change and then replies success/fail status
  189 + Try to change password and show success/fail status
189 190 '''
190 191 userid = self.current_user
191 192 passwd = self.get_body_arguments('new_password')[0]
192   -
193 193 changed_ok = await self.learn.change_password(userid, passwd)
194 194 if changed_ok:
195 195 notification = self.render_string(
... ... @@ -203,7 +203,6 @@ class ChangePasswordHandler(BaseHandler):
203 203 type='danger',
204 204 msg='A password não foi alterada!'
205 205 )
206   -
207 206 self.write({'msg': to_unicode(notification)})
208 207  
209 208  
... ... @@ -212,9 +211,10 @@ class RootHandler(BaseHandler):
212 211 '''
213 212 Handles root /
214 213 '''
  214 +
215 215 @tornado.web.authenticated
216 216 def get(self) -> None:
217   - '''Simply redirects to the main entrypoint'''
  217 + '''Redirect to main entrypoint'''
218 218 self.redirect('/courses')
219 219  
220 220  
... ... @@ -223,12 +223,13 @@ class CoursesHandler(BaseHandler):
223 223 '''
224 224 Handles /courses
225 225 '''
  226 +
226 227 def set_default_headers(self, *_) -> None:
227 228 self.set_header('Cache-Control', 'no-cache')
228 229  
229 230 @tornado.web.authenticated
230 231 def get(self) -> None:
231   - '''Renders list of available courses'''
  232 + '''Render available courses'''
232 233 uid = self.current_user
233 234 self.render('courses.html',
234 235 appname=APP_NAME,
... ... @@ -285,7 +286,7 @@ class TopicHandler(BaseHandler):
285 286 Starts a given topic
286 287 '''
287 288 uid = self.current_user
288   -
  289 + logger.debug('[TopicHandler.get] %s', topic)
289 290 try:
290 291 await self.learn.start_topic(uid, topic) # FIXME GET should not modify state...
291 292 except KeyError:
... ... @@ -304,15 +305,18 @@ class FileHandler(BaseHandler):
304 305 '''
305 306 Serves files from the /public subdir of the topics.
306 307 '''
  308 +
307 309 @tornado.web.authenticated
308 310 async def get(self, filename) -> None:
309 311 '''
310   - Serves files from /public subdirectories of a particular topic
  312 + Serve file from the /public subdirectory of a particular topic
311 313 '''
312 314 uid = self.current_user
313 315 public_dir = self.learn.get_current_public_dir(uid)
314 316 filepath = expanduser(join(public_dir, filename))
315 317  
  318 + logger.debug('uid=%s, public_dir=%s, filepath=%s', uid, public_dir,
  319 + filepath)
316 320 try:
317 321 with open(filepath, 'rb') as file:
318 322 data = file.read()
... ... @@ -350,10 +354,9 @@ class QuestionHandler(BaseHandler):
350 354 @tornado.web.authenticated
351 355 async def get(self) -> None:
352 356 '''
353   - Gets question to render.
354   - Shows an animated trophy if there are no more questions in the topic.
  357 + Get question to render or an animated trophy if no more questions.
355 358 '''
356   - logger.debug('[QuestionHandler]')
  359 + logger.debug('[QuestionHandler.get]')
357 360 user = self.current_user
358 361 question = await self.learn.get_question(user)
359 362  
... ... @@ -387,7 +390,7 @@ class QuestionHandler(BaseHandler):
387 390 @tornado.web.authenticated
388 391 async def post(self) -> None:
389 392 '''
390   - Corrects answer and returns status: right, wrong, try_again
  393 + Correct answer and return status: right, wrong, try_again
391 394 Does not move to next question.
392 395 '''
393 396 user = self.current_user
... ... @@ -422,18 +425,16 @@ class QuestionHandler(BaseHandler):
422 425 # --- check answer (nonblocking) and get corrected question and action
423 426 question = await self.learn.check_answer(user, ans)
424 427  
425   - # --- built response to return
  428 + # --- build response
426 429 response = {'method': question['status'], 'params': {}}
427 430  
428 431 if question['status'] == 'right': # get next question in the topic
429 432 comments = self.render_string('comments-right.html',
430 433 comments=question['comments'],
431 434 md=md_to_html)
432   -
433 435 solution = self.render_string('solution.html',
434 436 solution=question['solution'],
435 437 md=md_to_html)
436   -
437 438 response['params'] = {
438 439 'type': question['type'],
439 440 'progress': self.learn.get_student_progress(user),
... ... @@ -445,7 +446,6 @@ class QuestionHandler(BaseHandler):
445 446 comments = self.render_string('comments.html',
446 447 comments=question['comments'],
447 448 md=md_to_html)
448   -
449 449 response['params'] = {
450 450 'type': question['type'],
451 451 'progress': self.learn.get_student_progress(user),
... ... @@ -456,10 +456,8 @@ class QuestionHandler(BaseHandler):
456 456 comments = self.render_string('comments.html',
457 457 comments=question['comments'],
458 458 md=md_to_html)
459   -
460 459 solution = self.render_string(
461 460 'solution.html', solution=question['solution'], md=md_to_html)
462   -
463 461 response['params'] = {
464 462 'type': question['type'],
465 463 'progress': self.learn.get_student_progress(user),
... ... @@ -501,7 +499,7 @@ def run_webserver(app, ssl, port: int = 8443, debug: bool = False) -> None:
501 499 sys.exit(1)
502 500 logger.info('Web application started (tornado.web.Application)')
503 501  
504   - # --- create tornado webserver
  502 + # --- create tornado http server
505 503 try:
506 504 httpserver = tornado.httpserver.HTTPServer(webapp, ssl_options=ssl)
507 505 except ValueError:
... ... @@ -515,9 +513,11 @@ def run_webserver(app, ssl, port: int = 8443, debug: bool = False) -> None:
515 513 logger.critical('Cannot bind port %d. Already in use?', port)
516 514 sys.exit(1)
517 515 logger.info('Webserver listening on %d... (Ctrl-C to stop)', port)
  516 +
  517 + # --- set signal handler for Control-C
518 518 signal.signal(signal.SIGINT, signal_handler)
519 519  
520   - # --- run webserver
  520 + # --- run tornado webserver
521 521 try:
522 522 tornado.ioloop.IOLoop.current().start() # running...
523 523 except Exception:
... ...
aprendizations/templates/include-libs.html
1 1 <!-- jquery -->
2   -<script src="https://code.jquery.com/jquery-3.6.1.min.js" integrity="sha256-o88AwQnZB+VDvE9tvIXrMQaPlFFSUTR+nldQm1LuPXQ=" crossorigin="anonymous"></script>
  2 +<script src="https://code.jquery.com/jquery-3.6.3.min.js" integrity="sha256-pvPw+upLPUjgMXY0G+8O0xUf+/Im1MZjXxxgOcBQBXU=" crossorigin="anonymous"></script>
3 3  
4 4 <!-- bootstrap -->
5 5 <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous">
... ... @@ -7,3 +7,14 @@
7 7  
8 8 <!-- bootstrap icons -->
9 9 <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.2/font/bootstrap-icons.css">
  10 +
  11 +<!-- MathJax -->
  12 +<script src="https://polyfill.io/v3/polyfill.min.js?features=es6"></script>
  13 +<script async type="text/javascript" id="MathJax-script" src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
  14 +
  15 +<!-- codemirror -->
  16 +<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.63.3/codemirror.min.css" />
  17 +<script defer src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.63.3/codemirror.min.js"></script>
  18 +
  19 +<!-- animate -->
  20 +<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css" />
... ...
aprendizations/templates/question-checkbox.html
... ... @@ -3,13 +3,14 @@
3 3  
4 4 {% block answer %}
5 5 <fieldset data-role="controlgroup">
6   - <div class="list-group">
7   - {% for n,opt in enumerate(question['options']) %}
8   - <label class="list-group-item list-group-item-action">
9   - <input type="checkbox" class="form-check-input" id="{{ n }}" accesskey="{{ n+1 }}" name="answer" value="{{ n }}">
10   - {{ md(opt, strip_p_tag=True) }}
11   - </label>
12   - {% end %}
  6 + <div class="list-group">
  7 + {% for n,opt in enumerate(question['options']) %}
  8 + <li class="list-group-item">
  9 + <input class="form-check-input" type="checkbox" id="{{ n }}" accesskey="{{ n+1 }}" name="answer" value="{{ n }}">
  10 + <label class="form-check-label" for="{{ n }}">
  11 + {{ md(opt).removeprefix('<p>').removesuffix('</p>') }}
  12 + </label>
  13 + {% end %}
13 14 </div>
14 15 </fieldset>
15 16 <input type="hidden" name="qid" value="{{ question['qid'] }}">
... ...
aprendizations/templates/question-radio.html
... ... @@ -5,12 +5,12 @@
5 5 <fieldset data-role="controlgroup">
6 6 <div class="list-group">
7 7 {% for n,opt in enumerate(question['options']) %}
8   - <label class="list-group-item list-group-item-action">
9   - <input type="radio" class="form-check-input" id="{{ n }}" accesskey="{{ n+1 }}" name="answer" value="{{ n }}">
10   - <label for="{{ n }}" class="custom-control-label">
11   - {{ md(opt, strip_p_tag=True) }}
  8 + <li class="list-group-item">
  9 + <input class="form-check-input" type="radio" name="answer" value="{{ n }}" id="{{ n }}">
  10 + <label class="form-check-label stretched-link" for="{{ n }}">
  11 + {{ md(opt).removeprefix('<p>').removesuffix('</p>') }}
12 12 </label>
13   - </label>
  13 + </li>
14 14 {% end %}
15 15 </div>
16 16 </fieldset>
... ...
aprendizations/templates/topic.html
... ... @@ -8,27 +8,6 @@
8 8  
9 9 {% include include-libs.html %}
10 10  
11   - <!-- mathjax -->
12   - <script>
13   - MathJax = {
14   - tex: {
15   - inlineMath: [['$$$', '$$$'], ['\\(', '\\)']]
16   - },
17   - svg: {
18   - fontCache: 'global'
19   - }
20   - };
21   - </script>
22   - <script src="https://polyfill.io/v3/polyfill.min.js?features=es6"></script>
23   - <script async type="text/javascript" id="MathJax-script" src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
24   -
25   - <!-- codemirror -->
26   - <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.63.3/codemirror.min.css" />
27   - <script defer src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.63.3/codemirror.min.js"></script>
28   -
29   - <!-- animate -->
30   - <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css" />
31   -
32 11 <!-- local -->
33 12 <link rel="stylesheet" href="{{static_url('css/github.css')}}" />
34 13 <link rel="stylesheet" href="{{static_url('css/topic.css')}}" />
... ... @@ -99,7 +78,7 @@
99 78 <div id="solution_wrong"></div>
100 79 </div>
101 80 </div>
102   - <!-- button reponder / continuar -->
  81 + <!-- button responder / continuar -->
103 82 <div class="d-grid gap-2">
104 83 <button type="submit" class="btn btn-primary btn-lg btn-block my-5 shadow bg-gradient" id="submit" data-bs-toggle="button" href="#solution" style="display: none"></button>
105 84 </div>
... ...
aprendizations/tools.py
... ... @@ -3,15 +3,15 @@
3 3 import asyncio
4 4 import logging
5 5 from os import path
6   -import re
  6 +# import re
7 7 import subprocess
8 8 from typing import Any, List
9 9  
10 10 # third party libraries
11   -import mistune
12   -from pygments import highlight
13   -from pygments.lexers import get_lexer_by_name
14   -from pygments.formatters import HtmlFormatter
  11 +# import mistune
  12 +# from pygments import highlight
  13 +# from pygments.lexers import get_lexer_by_name
  14 +# from pygments.formatters import HtmlFormatter
15 15 import yaml
16 16  
17 17  
... ... @@ -28,119 +28,119 @@ logger = logging.getLogger(__name__)
28 28 # Block math:
29 29 # $$x$$ or \begin{equation}x\end{equation}
30 30 # -------------------------------------------------------------------------
31   -class MathBlockGrammar(mistune.BlockGrammar):
32   - block_math = re.compile(r'^\$\$(.*?)\$\$', re.DOTALL)
33   - latex_environment = re.compile(r'^\\begin\{([a-z]*\*?)\}(.*?)\\end\{\1\}',
34   - re.DOTALL)
35   -
36   -
37   -class MathBlockLexer(mistune.BlockLexer):
38   - default_rules = ['block_math', 'latex_environment'] \
39   - + mistune.BlockLexer.default_rules
40   -
41   - def __init__(self, rules=None, **kwargs):
42   - if rules is None:
43   - rules = MathBlockGrammar()
44   - super().__init__(rules, **kwargs)
45   -
46   - def parse_block_math(self, m):
47   - '''Parse a $$math$$ block'''
48   - self.tokens.append({
49   - 'type': 'block_math',
50   - 'text': m.group(1)
51   - })
52   -
53   - def parse_latex_environment(self, m):
54   - r'''Parse an environment \begin{name}text\end{name}'''
55   - self.tokens.append({
56   - 'type': 'latex_environment',
57   - 'name': m.group(1),
58   - 'text': m.group(2)
59   - })
60   -
61   -
62   -# -------------------------------------------------------------------------
63   -# Inline math: $x$
64   -# -------------------------------------------------------------------------
65   -class MathInlineGrammar(mistune.InlineGrammar):
66   - math = re.compile(r'^\$(.+?)\$', re.DOTALL)
67   - block_math = re.compile(r'^\$\$(.+?)\$\$', re.DOTALL)
68   - text = re.compile(r'^[\s\S]+?(?=[\\<!\[_*`~$]|https?://| {2,}\n|$)')
69   -
70   -
71   -class MathInlineLexer(mistune.InlineLexer):
72   - default_rules = ['block_math', 'math'] + mistune.InlineLexer.default_rules
73   -
74   - def __init__(self, renderer, rules=None, **kwargs):
75   - if rules is None:
76   - rules = MathInlineGrammar()
77   - super().__init__(renderer, rules, **kwargs)
78   -
79   - def output_math(self, m):
80   - return self.renderer.inline_math(m.group(1))
81   -
82   - def output_block_math(self, m):
83   - return self.renderer.block_math(m.group(1))
84   -
85   -
86   -class MarkdownWithMath(mistune.Markdown):
87   - def __init__(self, renderer, **kwargs):
88   - if 'inline' not in kwargs:
89   - kwargs['inline'] = MathInlineLexer
90   - if 'block' not in kwargs:
91   - kwargs['block'] = MathBlockLexer
92   - super().__init__(renderer, **kwargs)
93   -
94   - def output_block_math(self):
95   - return self.renderer.block_math(self.token['text'])
96   -
97   - def output_latex_environment(self):
98   - return self.renderer.latex_environment(self.token['name'],
99   - self.token['text'])
100   -
101   -
102   -class HighlightRenderer(mistune.Renderer):
103   - def block_code(self, code, lang='text'):
104   - try:
105   - lexer = get_lexer_by_name(lang, stripall=False)
106   - except Exception:
107   - lexer = get_lexer_by_name('text', stripall=False)
108   -
109   - formatter = HtmlFormatter()
110   - return highlight(code, lexer, formatter)
111   -
112   - def table(self, header, body):
113   - return ('<table class="table table-sm"><thead class="thead-light">'
114   - f'{header}</thead><tbody>{body}</tbody></table>')
115   -
116   - def image(self, src, title, alt):
117   - alt = mistune.escape(alt, quote=True)
118   - title = mistune.escape(title or '', quote=True)
119   - return (f'<img src="/file/{src}" alt="{alt}" title="{title}"'
120   - f'class="img-fluid">') # class="img-fluid mx-auto d-block"
121   -
122   - # Pass math through unaltered - mathjax does the rendering in the browser
123   - def block_math(self, text):
124   - return fr'\[ {text} \]'
125   -
126   - def latex_environment(self, name, text):
127   - return fr'\begin{{{name}}} {text} \end{{{name}}}'
128   -
129   - def inline_math(self, text):
130   - return fr'\( {text} \)'
131   -
132   -
133   -# hard_wrap=True to insert <br> on newline
134   -markdown = MarkdownWithMath(HighlightRenderer(escape=True))
135   -
136   -
137   -def md_to_html(text: str, strip_p_tag: bool = False) -> str:
138   - md: str = markdown(text)
139   - if strip_p_tag and md.startswith('<p>') and md.endswith('</p>\n'):
140   - return md[3:-5]
141   - else:
142   - return md
143   -
  31 +# class MathBlockGrammar(mistune.BlockGrammar):
  32 +# block_math = re.compile(r'^\$\$(.*?)\$\$', re.DOTALL)
  33 +# latex_environment = re.compile(r'^\\begin\{([a-z]*\*?)\}(.*?)\\end\{\1\}',
  34 +# re.DOTALL)
  35 +#
  36 +#
  37 +# class MathBlockLexer(mistune.BlockLexer):
  38 +# default_rules = ['block_math', 'latex_environment'] \
  39 +# + mistune.BlockLexer.default_rules
  40 +#
  41 +# def __init__(self, rules=None, **kwargs):
  42 +# if rules is None:
  43 +# rules = MathBlockGrammar()
  44 +# super().__init__(rules, **kwargs)
  45 +#
  46 +# def parse_block_math(self, m):
  47 +# '''Parse a $$math$$ block'''
  48 +# self.tokens.append({
  49 +# 'type': 'block_math',
  50 +# 'text': m.group(1)
  51 +# })
  52 +#
  53 +# def parse_latex_environment(self, m):
  54 +# r'''Parse an environment \begin{name}text\end{name}'''
  55 +# self.tokens.append({
  56 +# 'type': 'latex_environment',
  57 +# 'name': m.group(1),
  58 +# 'text': m.group(2)
  59 +# })
  60 +#
  61 +#
  62 +# # -------------------------------------------------------------------------
  63 +# # Inline math: $x$
  64 +# # -------------------------------------------------------------------------
  65 +# class MathInlineGrammar(mistune.InlineGrammar):
  66 +# math = re.compile(r'^\$(.+?)\$', re.DOTALL)
  67 +# block_math = re.compile(r'^\$\$(.+?)\$\$', re.DOTALL)
  68 +# text = re.compile(r'^[\s\S]+?(?=[\\<!\[_*`~$]|https?://| {2,}\n|$)')
  69 +#
  70 +#
  71 +# class MathInlineLexer(mistune.InlineLexer):
  72 +# default_rules = ['block_math', 'math'] + mistune.InlineLexer.default_rules
  73 +#
  74 +# def __init__(self, renderer, rules=None, **kwargs):
  75 +# if rules is None:
  76 +# rules = MathInlineGrammar()
  77 +# super().__init__(renderer, rules, **kwargs)
  78 +#
  79 +# def output_math(self, m):
  80 +# return self.renderer.inline_math(m.group(1))
  81 +#
  82 +# def output_block_math(self, m):
  83 +# return self.renderer.block_math(m.group(1))
  84 +#
  85 +#
  86 +# class MarkdownWithMath(mistune.Markdown):
  87 +# def __init__(self, renderer, **kwargs):
  88 +# if 'inline' not in kwargs:
  89 +# kwargs['inline'] = MathInlineLexer
  90 +# if 'block' not in kwargs:
  91 +# kwargs['block'] = MathBlockLexer
  92 +# super().__init__(renderer, **kwargs)
  93 +#
  94 +# def output_block_math(self):
  95 +# return self.renderer.block_math(self.token['text'])
  96 +#
  97 +# def output_latex_environment(self):
  98 +# return self.renderer.latex_environment(self.token['name'],
  99 +# self.token['text'])
  100 +#
  101 +#
  102 +# class HighlightRenderer(mistune.Renderer):
  103 +# def block_code(self, code, lang='text'):
  104 +# try:
  105 +# lexer = get_lexer_by_name(lang, stripall=False)
  106 +# except Exception:
  107 +# lexer = get_lexer_by_name('text', stripall=False)
  108 +#
  109 +# formatter = HtmlFormatter()
  110 +# return highlight(code, lexer, formatter)
  111 +#
  112 +# def table(self, header, body):
  113 +# return ('<table class="table table-sm"><thead class="thead-light">'
  114 +# f'{header}</thead><tbody>{body}</tbody></table>')
  115 +#
  116 +# def image(self, src, title, alt):
  117 +# alt = mistune.escape(alt, quote=True)
  118 +# title = mistune.escape(title or '', quote=True)
  119 +# return (f'<img src="/file/{src}" alt="{alt}" title="{title}"'
  120 +# f'class="img-fluid">') # class="img-fluid mx-auto d-block"
  121 +#
  122 +# # Pass math through unaltered - mathjax does the rendering in the browser
  123 +# def block_math(self, text):
  124 +# return fr'\[ {text} \]'
  125 +#
  126 +# def latex_environment(self, name, text):
  127 +# return fr'\begin{{{name}}} {text} \end{{{name}}}'
  128 +#
  129 +# def inline_math(self, text):
  130 +# return fr'\( {text} \)'
  131 +#
  132 +#
  133 +# # hard_wrap=True to insert <br> on newline
  134 +# markdown = MarkdownWithMath(HighlightRenderer(escape=True))
  135 +#
  136 +#
  137 +# def md_to_html(text: str, strip_p_tag: bool = False) -> str:
  138 +# md: str = markdown(text)
  139 +# if strip_p_tag and md.startswith('<p>') and md.endswith('</p>\n'):
  140 +# return md[3:-5]
  141 +# else:
  142 +# return md
  143 +#
144 144  
145 145 # ---------------------------------------------------------------------------
146 146 # load data from yaml file
... ...
setup.py
... ... @@ -21,7 +21,7 @@ setup(
21 21 python_requires='>=3.9.*',
22 22 install_requires=[
23 23 'tornado>=6.0',
24   - 'mistune<2',
  24 + 'mistune>=3',
25 25 'pyyaml>=5.1',
26 26 'pygments',
27 27 'sqlalchemy>=1.4',
... ...