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.
- pyodide_mkdocs_theme/__init__.py +34 -0
- pyodide_mkdocs_theme/__main__.py +166 -0
- pyodide_mkdocs_theme/__version__.py +1 -0
- pyodide_mkdocs_theme/pyodide_macros/__init__.py +21 -0
- pyodide_mkdocs_theme/pyodide_macros/exceptions.py +25 -0
- pyodide_mkdocs_theme/pyodide_macros/html_builder/__init__.py +45 -0
- pyodide_mkdocs_theme/pyodide_macros/html_builder/_html_builder.py +145 -0
- pyodide_mkdocs_theme/pyodide_macros/macros/IDEs/__init__.py +121 -0
- pyodide_mkdocs_theme/pyodide_macros/macros/IDEs/ide.py +576 -0
- pyodide_mkdocs_theme/pyodide_macros/macros/IDEs/ide_files_data.py +427 -0
- pyodide_mkdocs_theme/pyodide_macros/macros/__init__.py +19 -0
- pyodide_mkdocs_theme/pyodide_macros/macros/autres.py +226 -0
- pyodide_mkdocs_theme/pyodide_macros/macros/isolated_components.py +126 -0
- pyodide_mkdocs_theme/pyodide_macros/macros/qcm.py +318 -0
- pyodide_mkdocs_theme/pyodide_macros/pages_and_ides_configs.py +194 -0
- pyodide_mkdocs_theme/pyodide_macros/parsing.py +115 -0
- pyodide_mkdocs_theme/pyodide_macros/paths_utils.py +130 -0
- pyodide_mkdocs_theme/pyodide_macros/plugin/__init__.py +19 -0
- pyodide_mkdocs_theme/pyodide_macros/plugin/config.py +197 -0
- pyodide_mkdocs_theme/pyodide_macros/plugin/maestro_IDE.py +342 -0
- pyodide_mkdocs_theme/pyodide_macros/plugin/maestro_base_and_indent.py +324 -0
- pyodide_mkdocs_theme/pyodide_macros/plugin/maestro_extras.py +61 -0
- pyodide_mkdocs_theme/pyodide_macros/plugin/maestro_tools.py +94 -0
- pyodide_mkdocs_theme/pyodide_macros/plugin/pyodide_macros_plugin.py +256 -0
- pyodide_mkdocs_theme/pyodide_macros/pyodide_logger.py +99 -0
- pyodide_mkdocs_theme/pyodide_macros/scripts_templates.py +5 -0
- pyodide_mkdocs_theme/pyodide_macros/tools_and_constants.py +229 -0
- pyodide_mkdocs_theme/templates/__init__.py +19 -0
- pyodide_mkdocs_theme/templates/main.html +96 -0
- pyodide_mkdocs_theme/templates/mkdocs_theme.yml +39 -0
- pyodide_mkdocs_theme/templates/partials/copyright.html +24 -0
- pyodide_mkdocs_theme/templates/partials/footer.html +50 -0
- pyodide_mkdocs_theme/templates/partials/social.html +16 -0
- pyodide_mkdocs_theme/templates/pyodide-mkdocs/IDE-and-buttons/0_buttonsAndCounter-scripts.js +302 -0
- pyodide_mkdocs_theme/templates/pyodide-mkdocs/IDE-and-buttons/ace-editor-scripts.js +179 -0
- pyodide_mkdocs_theme/templates/pyodide-mkdocs/IDE-and-buttons/ide-extrahead.css +172 -0
- pyodide_mkdocs_theme/templates/pyodide-mkdocs/IDE-and-buttons/images/icons8-check-64.png +0 -0
- pyodide_mkdocs_theme/templates/pyodide-mkdocs/IDE-and-buttons/images/icons8-download-64.png +0 -0
- pyodide_mkdocs_theme/templates/pyodide-mkdocs/IDE-and-buttons/images/icons8-play-64.png +0 -0
- pyodide_mkdocs_theme/templates/pyodide-mkdocs/IDE-and-buttons/images/icons8-restart-64.png +0 -0
- pyodide_mkdocs_theme/templates/pyodide-mkdocs/IDE-and-buttons/images/icons8-save-64.png +0 -0
- pyodide_mkdocs_theme/templates/pyodide-mkdocs/IDE-and-buttons/images/icons8-upload-64.png +0 -0
- pyodide_mkdocs_theme/templates/pyodide-mkdocs/IDE-and-buttons/z_doLast-subscriptions-scripts.js +58 -0
- pyodide_mkdocs_theme/templates/pyodide-mkdocs/README.md +9 -0
- pyodide_mkdocs_theme/templates/pyodide-mkdocs/actions/0_tasks-scripts.js +194 -0
- pyodide_mkdocs_theme/templates/pyodide-mkdocs/actions/genericCodeRunner-scripts.js +461 -0
- pyodide_mkdocs_theme/templates/pyodide-mkdocs/error-logs-generator-scripts.js +260 -0
- pyodide_mkdocs_theme/templates/pyodide-mkdocs/js-libs/README.md +3 -0
- pyodide_mkdocs_theme/templates/pyodide-mkdocs/js-libs/globalsAndTools/0_config-libs.js +130 -0
- pyodide_mkdocs_theme/templates/pyodide-mkdocs/js-libs/globalsAndTools/functools-libs.js +213 -0
- pyodide_mkdocs_theme/templates/pyodide-mkdocs/js-libs/globalsAndTools/jsLogger-libs.js +57 -0
- pyodide_mkdocs_theme/templates/pyodide-mkdocs/js-libs/globalsAndTools/securedPagesData-libs.js +106 -0
- pyodide_mkdocs_theme/templates/pyodide-mkdocs/js-libs/globalsAndTools/z_globalGuiButtons-libs.js +66 -0
- pyodide_mkdocs_theme/templates/pyodide-mkdocs/js-libs/globalsAndTools/z_header-btns-extrahead.css +46 -0
- pyodide_mkdocs_theme/templates/pyodide-mkdocs/js-libs/mathjax-libs.js +54 -0
- pyodide_mkdocs_theme/templates/pyodide-mkdocs/perPageScripts/README.md +3 -0
- pyodide_mkdocs_theme/templates/pyodide-mkdocs/perPageScripts/start-pyodide.css +64 -0
- pyodide_mkdocs_theme/templates/pyodide-mkdocs/perPageScripts/start-pyodide.js +212 -0
- pyodide_mkdocs_theme/templates/pyodide-mkdocs/pyoditeur-extrahead.css +163 -0
- pyodide_mkdocs_theme/templates/pyodide-mkdocs/terminal/css/0_cdn-terminal-replacement-extrahead.css +1343 -0
- pyodide_mkdocs_theme/templates/pyodide-mkdocs/terminal/css/terminal-extrahead.css +56 -0
- pyodide_mkdocs_theme/templates/pyodide-mkdocs/terminal/terminal-helpers-scripts.js +172 -0
- pyodide_mkdocs_theme/templates/pyodide-mkdocs/terminal/terminal-scripts.js +159 -0
- pyodide_mkdocs_theme/templates/pyodide-mkdocs/terminal/z_doLast_terminal-scripts.js +50 -0
- pyodide_mkdocs_theme/templates/qcm/.qcm-circle.svg +76 -0
- pyodide_mkdocs_theme/templates/qcm/qcm-extrahead.css +174 -0
- pyodide_mkdocs_theme/templates/qcm/qcm-scripts.js +349 -0
- pyodide_mkdocs_theme-0.1.0.dist-info/LICENSE +674 -0
- pyodide_mkdocs_theme-0.1.0.dist-info/METADATA +64 -0
- pyodide_mkdocs_theme-0.1.0.dist-info/RECORD +72 -0
- pyodide_mkdocs_theme-0.1.0.dist-info/WHEEL +4 -0
- 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
|