pyodide-mkdocs-theme 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. pyodide_mkdocs_theme/__init__.py +34 -0
  2. pyodide_mkdocs_theme/__main__.py +166 -0
  3. pyodide_mkdocs_theme/__version__.py +1 -0
  4. pyodide_mkdocs_theme/pyodide_macros/__init__.py +21 -0
  5. pyodide_mkdocs_theme/pyodide_macros/exceptions.py +25 -0
  6. pyodide_mkdocs_theme/pyodide_macros/html_builder/__init__.py +45 -0
  7. pyodide_mkdocs_theme/pyodide_macros/html_builder/_html_builder.py +145 -0
  8. pyodide_mkdocs_theme/pyodide_macros/macros/IDEs/__init__.py +121 -0
  9. pyodide_mkdocs_theme/pyodide_macros/macros/IDEs/ide.py +576 -0
  10. pyodide_mkdocs_theme/pyodide_macros/macros/IDEs/ide_files_data.py +427 -0
  11. pyodide_mkdocs_theme/pyodide_macros/macros/__init__.py +19 -0
  12. pyodide_mkdocs_theme/pyodide_macros/macros/autres.py +226 -0
  13. pyodide_mkdocs_theme/pyodide_macros/macros/isolated_components.py +126 -0
  14. pyodide_mkdocs_theme/pyodide_macros/macros/qcm.py +318 -0
  15. pyodide_mkdocs_theme/pyodide_macros/pages_and_ides_configs.py +194 -0
  16. pyodide_mkdocs_theme/pyodide_macros/parsing.py +115 -0
  17. pyodide_mkdocs_theme/pyodide_macros/paths_utils.py +130 -0
  18. pyodide_mkdocs_theme/pyodide_macros/plugin/__init__.py +19 -0
  19. pyodide_mkdocs_theme/pyodide_macros/plugin/config.py +197 -0
  20. pyodide_mkdocs_theme/pyodide_macros/plugin/maestro_IDE.py +342 -0
  21. pyodide_mkdocs_theme/pyodide_macros/plugin/maestro_base_and_indent.py +324 -0
  22. pyodide_mkdocs_theme/pyodide_macros/plugin/maestro_extras.py +61 -0
  23. pyodide_mkdocs_theme/pyodide_macros/plugin/maestro_tools.py +94 -0
  24. pyodide_mkdocs_theme/pyodide_macros/plugin/pyodide_macros_plugin.py +256 -0
  25. pyodide_mkdocs_theme/pyodide_macros/pyodide_logger.py +99 -0
  26. pyodide_mkdocs_theme/pyodide_macros/scripts_templates.py +5 -0
  27. pyodide_mkdocs_theme/pyodide_macros/tools_and_constants.py +229 -0
  28. pyodide_mkdocs_theme/templates/__init__.py +19 -0
  29. pyodide_mkdocs_theme/templates/main.html +96 -0
  30. pyodide_mkdocs_theme/templates/mkdocs_theme.yml +39 -0
  31. pyodide_mkdocs_theme/templates/partials/copyright.html +24 -0
  32. pyodide_mkdocs_theme/templates/partials/footer.html +50 -0
  33. pyodide_mkdocs_theme/templates/partials/social.html +16 -0
  34. pyodide_mkdocs_theme/templates/pyodide-mkdocs/IDE-and-buttons/0_buttonsAndCounter-scripts.js +302 -0
  35. pyodide_mkdocs_theme/templates/pyodide-mkdocs/IDE-and-buttons/ace-editor-scripts.js +179 -0
  36. pyodide_mkdocs_theme/templates/pyodide-mkdocs/IDE-and-buttons/ide-extrahead.css +172 -0
  37. pyodide_mkdocs_theme/templates/pyodide-mkdocs/IDE-and-buttons/images/icons8-check-64.png +0 -0
  38. pyodide_mkdocs_theme/templates/pyodide-mkdocs/IDE-and-buttons/images/icons8-download-64.png +0 -0
  39. pyodide_mkdocs_theme/templates/pyodide-mkdocs/IDE-and-buttons/images/icons8-play-64.png +0 -0
  40. pyodide_mkdocs_theme/templates/pyodide-mkdocs/IDE-and-buttons/images/icons8-restart-64.png +0 -0
  41. pyodide_mkdocs_theme/templates/pyodide-mkdocs/IDE-and-buttons/images/icons8-save-64.png +0 -0
  42. pyodide_mkdocs_theme/templates/pyodide-mkdocs/IDE-and-buttons/images/icons8-upload-64.png +0 -0
  43. pyodide_mkdocs_theme/templates/pyodide-mkdocs/IDE-and-buttons/z_doLast-subscriptions-scripts.js +58 -0
  44. pyodide_mkdocs_theme/templates/pyodide-mkdocs/README.md +9 -0
  45. pyodide_mkdocs_theme/templates/pyodide-mkdocs/actions/0_tasks-scripts.js +194 -0
  46. pyodide_mkdocs_theme/templates/pyodide-mkdocs/actions/genericCodeRunner-scripts.js +461 -0
  47. pyodide_mkdocs_theme/templates/pyodide-mkdocs/error-logs-generator-scripts.js +260 -0
  48. pyodide_mkdocs_theme/templates/pyodide-mkdocs/js-libs/README.md +3 -0
  49. pyodide_mkdocs_theme/templates/pyodide-mkdocs/js-libs/globalsAndTools/0_config-libs.js +130 -0
  50. pyodide_mkdocs_theme/templates/pyodide-mkdocs/js-libs/globalsAndTools/functools-libs.js +213 -0
  51. pyodide_mkdocs_theme/templates/pyodide-mkdocs/js-libs/globalsAndTools/jsLogger-libs.js +57 -0
  52. pyodide_mkdocs_theme/templates/pyodide-mkdocs/js-libs/globalsAndTools/securedPagesData-libs.js +106 -0
  53. pyodide_mkdocs_theme/templates/pyodide-mkdocs/js-libs/globalsAndTools/z_globalGuiButtons-libs.js +66 -0
  54. pyodide_mkdocs_theme/templates/pyodide-mkdocs/js-libs/globalsAndTools/z_header-btns-extrahead.css +46 -0
  55. pyodide_mkdocs_theme/templates/pyodide-mkdocs/js-libs/mathjax-libs.js +54 -0
  56. pyodide_mkdocs_theme/templates/pyodide-mkdocs/perPageScripts/README.md +3 -0
  57. pyodide_mkdocs_theme/templates/pyodide-mkdocs/perPageScripts/start-pyodide.css +64 -0
  58. pyodide_mkdocs_theme/templates/pyodide-mkdocs/perPageScripts/start-pyodide.js +212 -0
  59. pyodide_mkdocs_theme/templates/pyodide-mkdocs/pyoditeur-extrahead.css +163 -0
  60. pyodide_mkdocs_theme/templates/pyodide-mkdocs/terminal/css/0_cdn-terminal-replacement-extrahead.css +1343 -0
  61. pyodide_mkdocs_theme/templates/pyodide-mkdocs/terminal/css/terminal-extrahead.css +56 -0
  62. pyodide_mkdocs_theme/templates/pyodide-mkdocs/terminal/terminal-helpers-scripts.js +172 -0
  63. pyodide_mkdocs_theme/templates/pyodide-mkdocs/terminal/terminal-scripts.js +159 -0
  64. pyodide_mkdocs_theme/templates/pyodide-mkdocs/terminal/z_doLast_terminal-scripts.js +50 -0
  65. pyodide_mkdocs_theme/templates/qcm/.qcm-circle.svg +76 -0
  66. pyodide_mkdocs_theme/templates/qcm/qcm-extrahead.css +174 -0
  67. pyodide_mkdocs_theme/templates/qcm/qcm-scripts.js +349 -0
  68. pyodide_mkdocs_theme-0.1.0.dist-info/LICENSE +674 -0
  69. pyodide_mkdocs_theme-0.1.0.dist-info/METADATA +64 -0
  70. pyodide_mkdocs_theme-0.1.0.dist-info/RECORD +72 -0
  71. pyodide_mkdocs_theme-0.1.0.dist-info/WHEEL +4 -0
  72. pyodide_mkdocs_theme-0.1.0.dist-info/entry_points.txt +6 -0
@@ -0,0 +1,576 @@
1
+ """
2
+ pyodide-mkdocs-theme
3
+ Copyleft GNU GPLv3 🄯 2024 Frédéric Zinelli
4
+
5
+ This program is free software: you can redistribute it and/or modify
6
+ it under the terms of the GNU General Public License as published by
7
+ the Free Software Foundation, either version 3 of the License, or
8
+ (at your option) any later version.
9
+
10
+ This program is distributed in the hope that it will be useful,
11
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
13
+ See the GNU General Public License for more details.
14
+
15
+ You should have received a copy of the GNU General Public License
16
+ along with this program.
17
+ If not, see <https://www.gnu.org/licenses/>.
18
+ """
19
+
20
+ """
21
+ Macros to insert IDE (aka. editor + terminal + buttons)
22
+ """
23
+ # pylint: disable=unused-argument
24
+
25
+
26
+ import re
27
+ import hashlib
28
+ from typing import Any, ClassVar, Dict, List, Literal, Optional, Tuple, Union
29
+ from dataclasses import dataclass
30
+ from pathlib import Path
31
+ from math import inf
32
+
33
+ from mkdocs.exceptions import BuildError
34
+
35
+ from ... import html_builder as Html
36
+ from ...tools_and_constants import HtmlClass, Prefix, ScriptKind
37
+ from ...pages_and_ides_configs import IdeConfigKey
38
+ from ...parsing import build_code_fence
39
+ from ...paths_utils import convert_url_to_utf8, to_uri
40
+ from ...plugin.maestro_IDE import MaestroIDE
41
+
42
+ from .ide_files_data import IdeFilesExtractor
43
+
44
+
45
+
46
+
47
+ #---------------------------------------------------------------------------------
48
+
49
+
50
+
51
+
52
+
53
+ @dataclass
54
+ class Ide:
55
+ """
56
+ Builds an editor + a terminal + the buttons and extra logistic needed for them.
57
+ """
58
+
59
+ # Defined on instantiation:
60
+ #--------------------------
61
+
62
+ my_env: MaestroIDE
63
+ """ The MaestroEnv singleton """
64
+
65
+ script_name: str
66
+ """ Base name for the files to use (first argument passed to the macros)
67
+ Partial path from the directory holding the sujet.md file, to the one holding all the
68
+ other required files, ending with the common prefix for the exercice.
69
+ Ex: "exo" to extract: "exo.py", "exo_corr.py", "exo_test.py", ...
70
+ "sub_exA/exo" for: "sub_exA/exo.py", "sub_exA/exo_corr.py", ...
71
+ """
72
+
73
+ mode: Union[Literal[""],Literal["_v"]]
74
+ """ The terminal will be below (mode="") or on the right (mode="_v") of the editor.
75
+ (what an awful interface, yeah... x) )
76
+ """
77
+
78
+ max_attempts: Union[int, Literal["+"]]
79
+ """ Maximum number of attempts before the solution admonition will become available """
80
+
81
+ excluded: str
82
+ """ String of spaces or coma separated python functions or modules/packages that are forbidden
83
+ at runtime. By default, nothing is forbidden.
84
+ - Every string section that matches a builtin callable forbid that function by
85
+ replacing it with another function which will raise an error if called.
86
+ - Every string section prefixed with a fot forbids a method call. Here a simple
87
+ string containment check is done opn the user's code, to check it does not
88
+ contain the desired method name with the dot before it.
89
+ - Any other string section is considered as a module name and doing an import (in
90
+ any way/syntax) involving that name will raise an error.
91
+
92
+ Note that the restrictions are rather strict, and may have unexpected side effects, such
93
+ as, forbidding `exec` will also forbid to import numpy, because the package relies on exec
94
+ for some operations at import time.
95
+ To circumvent such a kind of problems, use the white_list argument.
96
+ """
97
+
98
+ size: int
99
+ """ Max height of the editor (in number of lines) """
100
+
101
+ white_list: str
102
+ """ String of spaces or coma separated python modules/packages names the have to be
103
+ preloaded before the code restrictions are enforced on the user's side.
104
+ """
105
+
106
+ id: Optional[int]
107
+ """ Used to disambiguate the ids of two IDEs, if the same file is used several times
108
+ in the document.
109
+ """
110
+
111
+ auto_log_assert: Optional[bool]
112
+ """ If True, failing assertions without feedback during the private tests will be
113
+ augmented automatically with the code of the assertion itself. If None, use
114
+ the global `show_code_on_failed_assertions` plugin value and defined in
115
+ `CONFIG.showFailedAssertionsOnValidation` in the JS runtime.
116
+ """
117
+
118
+ rec_limit: int
119
+ """ If used, the recursion limit of the pyodide runtime will be updated before the user's
120
+ code or the tests are run.
121
+ Note that this also forbids the use of the `sys.setrecurionlimit` at runtime.
122
+ """
123
+
124
+ term_height: int
125
+ """ Number of lines to define the height of the terminal (unless it's vertical) """
126
+
127
+
128
+ # defined during post_init
129
+ #-------------------------
130
+
131
+
132
+ files_data: IdeFilesExtractor = None
133
+
134
+ editor_name: str = ''
135
+ """ tail part of most ids, in the shape of 'editor_{32 bits hexadecimal}' """
136
+
137
+ max_attempts_symbol: str = ''
138
+ """ Actual string representation to use when creating the counter under the IDE """
139
+
140
+ indentation: str = ''
141
+ """ Indentation on the left of the macro call, as str """
142
+
143
+
144
+ @property
145
+ def has_corr(self):
146
+ return self.files_data.has_corr
147
+ @property
148
+ def has_rem(self):
149
+ return self.files_data.has_rem
150
+ @property
151
+ def has_test(self):
152
+ return self.files_data.has_test
153
+
154
+
155
+
156
+ MIN_IDE_ID_DIGITS: ClassVar[str] = 8
157
+
158
+ INFINITY_SYMBOL: ClassVar[str] = "∞"
159
+
160
+ # (width (in em),tooltip). If width is 0, use default/automatic width
161
+ AVAILABLE_BUTTONS: ClassVar[Dict[str,Tuple[str,str]]] = {
162
+ "play": (9, "Exécuter le code<br>(<kbd>Ctrl</kbd>+<kbd>S</kbd>)"),
163
+ "check": (9, "Valider<br>(<kbd>Ctrl</kbd>+<kbd>Enter</kbd>)"),
164
+ "download": (0, "Télécharger"),
165
+ "upload": (0, "Téléverser"),
166
+ "restart": (0, "Réinitialiser l'éditeur"),
167
+ "save": (0, "Sauvegarder dans le navigateur"),
168
+ "###": (15, "(Dés-)Active le code après la ligne <code># Tests</code> "
169
+ "(insensible à la casse ; <kbd>Ctrl</kbd>+<kbd>I</kbd>)"),
170
+ }
171
+
172
+ ICON_TEMPLATE: ClassVar[str] = (
173
+ "{lvl_up}/pyodide-mkdocs/IDE-and-buttons/images/icons8-{button_name}-64.png"
174
+ )
175
+
176
+
177
+
178
+
179
+ def __post_init__(self):
180
+
181
+ self.files_data = IdeFilesExtractor(self.my_env, self.script_name, self.id)
182
+
183
+ if 0 <= self.rec_limit < self.my_env.MIN_RECURSION_LIMIT:
184
+ with_id = f' (ID={ self.id })' if self.id is not None else ''
185
+ raise BuildError(
186
+ f"The recursion limit for {self.my_env.page.file.src_uri}:{self.script_name}"
187
+ f"{with_id} is set too low and may causes runtime troubles. Please set it to "
188
+ f"at least { self.my_env.MIN_RECURSION_LIMIT }."
189
+ )
190
+
191
+ if self.id is not None and not isinstance(self.id, int):
192
+ raise BuildError(f'The ID argument should be an integer, but was: {self.id!r}')
193
+
194
+
195
+ # Extract python content and compute editor name:
196
+ exo_py: Optional[Path] = self.files_data.exo_py
197
+ id_ide: str = self._generate_id_ide(exo_py)
198
+ self.editor_name = f"{ Prefix.editor_ }{ id_ide }"
199
+
200
+
201
+ # Extract max number of attempts from file or macro argument, clean up the file if needed,
202
+ # then pick the correct number of attempts and set it in the global structure.
203
+ # Also defines self.ide_content.
204
+ max_attempts = self._define_max_attempts_symbols_and_value()
205
+
206
+
207
+ # Compute all code exclusions and white list of imports:
208
+ white_list = self._compute_exclusions_and_white_list("white_list")
209
+ excluded = self._compute_exclusions_and_white_list("excluded")
210
+ excluded_methods = [ meth for meth in excluded if meth.startswith('.') ]
211
+ excluded = [ no_meth for no_meth in excluded if not no_meth.startswith('.') ]
212
+
213
+
214
+ # Search the indentation level for the current IDE:
215
+ is_v = self.mode.strip('_')
216
+ quotes = """['"]"""
217
+ script_pattern = "" if not self.script_name else f"{quotes}{ self.script_name }{quotes}"
218
+ id_pattern = r'(?!.*?ID\s*=)' if self.id is None else rf".*?ID\s*=\s*{ self.id }\b"
219
+
220
+ ide_jinja_reg = re.compile( rf"IDE{ is_v }\(\s*{ script_pattern }{ id_pattern }" )
221
+ self.indentation = self.my_env.get_indent_in_current_page(ide_jinja_reg)
222
+
223
+
224
+
225
+ to_register: List[Tuple[IdeConfigKey,Any]] = [
226
+ ('hdr_content', self.files_data.hdr),
227
+ ('user_content', self.files_data.user_content),
228
+ ('public_tests', self.files_data.public_tests),
229
+ ('secret_tests', self.files_data.secret_tests),
230
+ ('corr_rem_config', self.files_data.corr_rem_bit_mask),
231
+ ('attempts_left', max_attempts),
232
+ ("excluded", excluded),
233
+ ("excluded_methods", excluded_methods),
234
+ ("white_list", white_list),
235
+ ("auto_log_assert", self.auto_log_assert),
236
+ ("rec_limit", self.rec_limit),
237
+ ]
238
+ for field,value in to_register:
239
+ self.my_env.set_current_page_js_data(self.editor_name, field, value)
240
+
241
+
242
+
243
+ #-----------------------------------------------------------------------------
244
+
245
+
246
+
247
+ def _compute_exclusions_and_white_list(self, prop:str):
248
+ """
249
+ Convert a string argument (exclusions or white list) tot he equivalent list of data.
250
+ """
251
+ rule = (getattr(self, prop) or "").strip(' ;,') # (never allow None)
252
+ lst = re.split(r'[ ;,]+', rule) if rule else []
253
+ return lst
254
+
255
+
256
+
257
+ def _generate_id_ide(self, py_path:Optional[Path]):
258
+ """
259
+ Generate an id number for the current IDE (editor+terminal), as a "prefix_hash(32bits)".
260
+
261
+ This id must be:
262
+ - Unique to every IDE used throughout the whole website.
263
+ - Stable, so that it can be used to identify what IDE goes with what file or what
264
+ localeStorage data.
265
+
266
+ Current strategy:
267
+ - If the file exists, hash its path.
268
+ - If there is no file, use the current global IDE_counter and hash its value as string.
269
+ - The "mode" of the IDE is appended to the string before hashing.
270
+ - Any ID value (macro argument) is also appended to the string before hashing.
271
+
272
+ Uniqueness of the resulting hash is verified and a BuildError is raised if two identical
273
+ hashes are encountered.
274
+ """
275
+ if py_path:
276
+ path = str(py_path)
277
+ else:
278
+ path = str(self.my_env.ide_count)
279
+
280
+ if self.mode:
281
+ path += self.mode
282
+
283
+ if self.id is not None:
284
+ path += str(self.id)
285
+
286
+ id_ide = hashlib.sha1(path.encode("utf-8")).hexdigest()
287
+
288
+ if not self.my_env.register_if_unique(id_ide):
289
+ raise BuildError(
290
+ "The same editor ID got generated twice. If you are trying to use the same set"
291
+ "of files for different IDEs, use the ID argument to disambiguate their id.\n"
292
+ f" Problematic file: { py_path }\n"
293
+ f" Possible solution: { '{{' } IDE(\"{ self.script_name }\", ID=2) { '}}' }"
294
+ )
295
+ return id_ide
296
+
297
+
298
+
299
+ def _define_max_attempts_symbols_and_value(self):
300
+ """
301
+ Any MAX value defined in the file takes precedence, because it's not possible to know
302
+ if the value coming from the macro is the default one or not.
303
+ """
304
+ max_ide = str(self.max_attempts) # from macro call
305
+
306
+ # If something about MAX in the file, it has precedence (if exists -> legacy...)
307
+ max_from_file = self.files_data.file_max_attempts
308
+ if max_from_file != "":
309
+ max_ide = "+" if max_from_file == "+" else max_from_file
310
+
311
+ is_inf = max_ide in ("+", "1000") # 1000: legacy...
312
+
313
+ # If ever there are neither correction nor remark, or if no tests, use also inf:
314
+ is_inf = is_inf or not (self.has_corr or self.has_rem) or not self.has_test
315
+
316
+ self.max_attempts_symbol = self.INFINITY_SYMBOL if is_inf else str(max_ide)
317
+
318
+ max_attempts = inf if is_inf else int(max_ide)
319
+ return max_attempts
320
+
321
+
322
+
323
+
324
+
325
+ #-----------------------------------------------------------------------------
326
+
327
+
328
+
329
+
330
+ def make_ide(self) -> str:
331
+ """
332
+ Create an IDE (Editor+Terminal) within an Mkdocs document. {script_name}.py is loaded on
333
+ the editor if present.
334
+ NOTES:
335
+ - Two modes are available : vertical or horizontal. Buttons are added through
336
+ functional calls.
337
+ - The last span hides the code content of the IDE when loaded.
338
+ """
339
+ # Mark the page as needing this kind of scripts in the body section:
340
+ self.my_env.set_current_page_insertion_needs(ScriptKind.pyodide)
341
+
342
+ ide_layout = self.generate_empty_ide()
343
+ buttons = self.generate_row_of_buttons()
344
+ global_layout = Html.div(
345
+ ide_layout+buttons,
346
+ id = f"{ Prefix.global_ }{ self.editor_name }",
347
+ kls = HtmlClass.py_mk_ide,
348
+ )
349
+ solution_div = self.__build_corr_and_rem()
350
+
351
+ return global_layout + solution_div
352
+
353
+
354
+
355
+ def generate_empty_ide(self) -> str:
356
+ """
357
+ Generate the global layout that will receive later the ace elements.
358
+ """
359
+ is_v = self.mode == '_v'
360
+ toggle_txt = '###'
361
+ tip_width,tip_text = self.AVAILABLE_BUTTONS[toggle_txt]
362
+
363
+ shortcut_comment_asserts = Html.span(
364
+ toggle_txt + Html.tooltip(tip_text, tip_width, shift=75),
365
+ id = Prefix.comment_ + self.editor_name,
366
+ kls = 'comment tooltip',
367
+ )
368
+ editor_div = Html.div(
369
+ id = self.editor_name,
370
+ is_v = str(is_v).lower(),
371
+ mode = self.mode,
372
+ max_size = self.size,
373
+ script_name = self.script_name,
374
+ )
375
+ editor_wrapper = Html.div(
376
+ editor_div + shortcut_comment_asserts,
377
+ kls = Prefix.comment_ + HtmlClass.py_mk_wrapper
378
+ )
379
+
380
+ separator = Html.div(
381
+ kls= HtmlClass.ide_separator + self.mode
382
+ )
383
+ terminal_div = Html.terminal(
384
+ Prefix.term_ + self.editor_name ,
385
+ kls = f"{ HtmlClass.term_editor }{ self.mode } { HtmlClass.py_mk_terminal }",
386
+ n_lines_h = self.term_height * (not is_v),
387
+ is_v = is_v,
388
+ )
389
+
390
+ return Html.div(
391
+ f"{ editor_wrapper }{ separator }{ terminal_div }",
392
+ kls = f"{ HtmlClass.py_mk_wrapper }{ self.mode }",
393
+ )
394
+
395
+
396
+
397
+
398
+
399
+ def __build_corr_and_rem(self):
400
+ """
401
+ Build the correction and REM holders. The rendered template is something like the
402
+ following, with the indentation level of the most outer div equal to the indentation
403
+ level of the IDE macro text in the markdown file.
404
+ Depending on the presence/absence of corr and REM file, some parts may be missing:
405
+
406
+ <div markdown="1" id="solution_editor_id" <<< ALWAYS
407
+ class="py_mk_hidden" >
408
+
409
+ ENCRYPTION_TOKEN <<< at least one and encryption ON
410
+
411
+ ??? tip "Solution" <<< at least one
412
+
413
+ <div markdown="1" style="padding:1em"> <<< at least one
414
+
415
+ ```python linenums="1"' <<< solution
416
+ --8<-- "{ corr_uri }" <<< solution
417
+ ``` <<< solution
418
+
419
+ ___Remarques :___ <<< remark & solution
420
+
421
+ --8<-- "{ rem_uri }" <<< remark
422
+
423
+ </div> <<< at least one
424
+
425
+ ENCRYPTION_TOKEN <<< at least one and encryption ON
426
+
427
+ </div> <<< ALWAYS
428
+
429
+ Don't forget that empty lines are mandatory to render the "md in html" as expected.
430
+ """
431
+
432
+ # Prepare data first (to ease reading of the below sections)
433
+ title_chunks = ['Solution'*self.has_corr, 'Remarques'*self.has_rem]
434
+ sol_title = ' & '.join(filter(bool, title_chunks))
435
+ corr_content = self.files_data.corr_content
436
+
437
+ at_least_one = self.has_corr or self.has_rem
438
+ with_encrypt = self.my_env.encryptCorrectionsAndRems and at_least_one
439
+ extra_tokens = ( self.my_env.ENCRYPTION_TOKEN, ) * with_encrypt
440
+
441
+
442
+ # Build the whole div content:
443
+ md_div = [ '', # Extra empty line to enforce proper rendering of the md around
444
+ f'<div markdown="1" id="{ Prefix.solution_ }{ self.editor_name }" '
445
+ f' class="{ HtmlClass.py_mk_hidden }" data-search-exclude >',
446
+ *extra_tokens ]
447
+ if at_least_one:
448
+ md_div.append( f'??? tip "{ sol_title }"' )
449
+ md_div.append( ' <div markdown="1" style="margin:1.7em 1em" >' )
450
+
451
+ if self.has_corr:
452
+ # first indentation must be removed, EXCEPT one level, because handled lower
453
+ # for the whole block
454
+ one_level = ' '
455
+ fence = build_code_fence(corr_content, one_level + self.indentation)
456
+ md_div.append( one_level+fence.strip())
457
+
458
+ if self.has_corr and self.has_rem:
459
+ md_div.append( f' <span class="{ HtmlClass.rem_fake_h3 }">Remarques :</span>')
460
+
461
+ if self.has_rem:
462
+ rem_uri = to_uri( convert_url_to_utf8(str(self.files_data.rem_rel_path)) )
463
+ md_div.append( f' --8<-- "{ rem_uri }"' )
464
+
465
+ if at_least_one:
466
+ md_div.append( ' </div>')
467
+
468
+ md_div.extend(( *extra_tokens,
469
+ '</div>\n', ))
470
+ # The extra linefeed is there to enforce md rendering of the following sections
471
+
472
+ # Add extra indentation according to IDE's insertion:
473
+ if self.indentation:
474
+ md_div = [ s and self.indentation+s for s in md_div ]
475
+
476
+ # Join every item with extra gaps, to following md rendering requirements
477
+ out = '\n\n'.join(md_div)
478
+ return out
479
+
480
+
481
+
482
+
483
+ def generate_row_of_buttons(self) -> str:
484
+ """
485
+ Build all buttons below an "ide" (editor+terminal).
486
+ """
487
+ buttons = [
488
+ self.create_button("play"),
489
+ self.create_validation_button(),
490
+ self.create_button("download", margin_left=1 ),
491
+ self.create_upload_button(margin_right=1),
492
+ self.create_button("restart"),
493
+ self.create_button("save"),
494
+ ]
495
+ return ''.join(buttons)
496
+
497
+
498
+
499
+ def create_button(
500
+ self, button_name: str,
501
+ *,
502
+ btn_kind:str=None,
503
+ margin_left:float=0.2, margin_right:float=0.2,
504
+ extra_content:str = "",
505
+ **kwargs
506
+ ) -> str:
507
+ """
508
+ Build one button, given its name.
509
+
510
+ @btn_kind: The name of the JS function to bind the button click event to. If not
511
+ given, use the lowercase version of button_name.
512
+ @margin_...: CSS formatting as floats (em units). By default, 0.2 on each side.
513
+ @extra_content: Allow to inject some additional html inside the button tag.
514
+ @**kwargs: All the remaining kwargs are attributes added to the button tag.
515
+ """
516
+ if btn_kind is None:
517
+ btn_kind = button_name.lower()
518
+
519
+ tooltip_width,tooltip_text = self.AVAILABLE_BUTTONS[button_name]
520
+ span_tooltip = Html.tooltip(tooltip_text, tooltip_width)
521
+ lvl_up = self.my_env.level_up_from_current_page()
522
+ img_link = self.ICON_TEMPLATE.format(lvl_up=lvl_up, button_name=button_name)
523
+ img = Html.img(src=img_link)
524
+ button_html = Html.button(
525
+ f'{ img }{ span_tooltip }{ extra_content }',
526
+ kls = 'tooltip',
527
+ btn_kind = btn_kind,
528
+ style = f"margin-left:{margin_left}em; margin-right:{margin_right}em;",
529
+ **kwargs,
530
+ type='button',
531
+ # https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#notes
532
+ )
533
+ return button_html
534
+
535
+
536
+
537
+ def create_upload_button(self, margin_right:float = 1) -> str:
538
+ """
539
+ @brief : Create upload button for an IDE number {id_ide}.
540
+ @details : Use an HTML input to upload a file from user. The user clicks on the button to
541
+ fire a JS event that triggers the hidden input.
542
+ """
543
+
544
+ input_button_controller = Html.input(
545
+ id = f"{ Prefix.input_ }{ self.editor_name }",
546
+ kls = HtmlClass.py_mk_hidden,
547
+ type = 'file',
548
+ name = 'file',
549
+ enctype = "multipart/form-data",
550
+ )
551
+ button = self.create_button(
552
+ "upload",
553
+ margin_right = margin_right,
554
+ extra_content = input_button_controller,
555
+ )
556
+ return button
557
+
558
+
559
+
560
+ def create_validation_button(self) -> str:
561
+ """
562
+ @brief: Generate the button for IDE {id_ide} to perform the unit tests if a valid
563
+ test_script.py is present.
564
+ @details: Hide the content in a div that is called in the Javascript
565
+ """
566
+ html_validation_button = ""
567
+ if self.has_test:
568
+ html_validation_button = self.create_button("check", btn_kind="validate")
569
+
570
+ # The counter is always present:
571
+ span_attempts_counter = Html.span(
572
+ f"{ self.max_attempts_symbol }/{ self.max_attempts_symbol }",
573
+ id=f'{ Prefix.compteur_ }{ self.editor_name }',
574
+ kls="compteur", # get rid of that? (probably)
575
+ )
576
+ return html_validation_button + span_attempts_counter