munchboka-edutools 0.1.13__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.

Potentially problematic release.


This version of munchboka-edutools might be problematic. Click here for more details.

Files changed (149) hide show
  1. munchboka_edutools/__init__.py +184 -0
  2. munchboka_edutools/_plotmath_shim.py +126 -0
  3. munchboka_edutools/_version.py +2 -0
  4. munchboka_edutools/directives/__init__.py +1 -0
  5. munchboka_edutools/directives/admonitions.py +389 -0
  6. munchboka_edutools/directives/cas_popup.py +272 -0
  7. munchboka_edutools/directives/clear.py +103 -0
  8. munchboka_edutools/directives/dialogue.py +137 -0
  9. munchboka_edutools/directives/escape_room.py +296 -0
  10. munchboka_edutools/directives/factor_tree.py +549 -0
  11. munchboka_edutools/directives/ggb.py +209 -0
  12. munchboka_edutools/directives/ggb_icon.py +105 -0
  13. munchboka_edutools/directives/ggb_popup.py +165 -0
  14. munchboka_edutools/directives/horner.py +324 -0
  15. munchboka_edutools/directives/interactive_code.py +75 -0
  16. munchboka_edutools/directives/jeopardy.py +252 -0
  17. munchboka_edutools/directives/multi_plot.py +1126 -0
  18. munchboka_edutools/directives/pair_puzzle.py +191 -0
  19. munchboka_edutools/directives/parsons.py +109 -0
  20. munchboka_edutools/directives/plot.py +3105 -0
  21. munchboka_edutools/directives/poly_icon.py +111 -0
  22. munchboka_edutools/directives/polydiv.py +344 -0
  23. munchboka_edutools/directives/popup.py +245 -0
  24. munchboka_edutools/directives/quiz.py +291 -0
  25. munchboka_edutools/directives/signchart.py +516 -0
  26. munchboka_edutools/directives/timed_quiz.py +436 -0
  27. munchboka_edutools/directives/turtle.py +157 -0
  28. munchboka_edutools/static/css/admonitions.css +2012 -0
  29. munchboka_edutools/static/css/cas_popup.css +242 -0
  30. munchboka_edutools/static/css/code_mirror_themes/github_dark_cm.css +112 -0
  31. munchboka_edutools/static/css/code_mirror_themes/github_dark_default_cm.css +40 -0
  32. munchboka_edutools/static/css/code_mirror_themes/github_dark_high_contrast_cm.css +141 -0
  33. munchboka_edutools/static/css/code_mirror_themes/github_light_cm.css +120 -0
  34. munchboka_edutools/static/css/code_mirror_themes/github_light_default_cm.css +108 -0
  35. munchboka_edutools/static/css/code_mirror_themes/one_dark_cm.css +121 -0
  36. munchboka_edutools/static/css/dialogue.css +92 -0
  37. munchboka_edutools/static/css/escapeRoom/escape-room.css +223 -0
  38. munchboka_edutools/static/css/figures.css +274 -0
  39. munchboka_edutools/static/css/general_style.css +74 -0
  40. munchboka_edutools/static/css/github-dark-high-contrast.css +141 -0
  41. munchboka_edutools/static/css/github-dark.css +112 -0
  42. munchboka_edutools/static/css/github-light.css +120 -0
  43. munchboka_edutools/static/css/interactive_code/style.css +575 -0
  44. munchboka_edutools/static/css/interactive_code.css +582 -0
  45. munchboka_edutools/static/css/jeopardy.css +529 -0
  46. munchboka_edutools/static/css/pairPuzzle/style.css +177 -0
  47. munchboka_edutools/static/css/parsons/parsonsPuzzle.css +331 -0
  48. munchboka_edutools/static/css/popup.css +115 -0
  49. munchboka_edutools/static/css/quiz.css +312 -0
  50. munchboka_edutools/static/css/timedQuiz.css +375 -0
  51. munchboka_edutools/static/icons/ggb/mode_evaluate.svg +1 -0
  52. munchboka_edutools/static/icons/ggb/mode_extremum.svg +1 -0
  53. munchboka_edutools/static/icons/ggb/mode_intersect.svg +1 -0
  54. munchboka_edutools/static/icons/ggb/mode_nsolve.svg +1 -0
  55. munchboka_edutools/static/icons/ggb/mode_numeric.svg +1 -0
  56. munchboka_edutools/static/icons/ggb/mode_point.svg +1 -0
  57. munchboka_edutools/static/icons/ggb/mode_solve.svg +1 -0
  58. munchboka_edutools/static/icons/misc/windows-logo.svg +1 -0
  59. munchboka_edutools/static/icons/outline/dark_mode/academic.svg +3 -0
  60. munchboka_edutools/static/icons/outline/dark_mode/backward.svg +3 -0
  61. munchboka_edutools/static/icons/outline/dark_mode/book.svg +3 -0
  62. munchboka_edutools/static/icons/outline/dark_mode/chat_bubble.svg +3 -0
  63. munchboka_edutools/static/icons/outline/dark_mode/check.svg +3 -0
  64. munchboka_edutools/static/icons/outline/dark_mode/cmd_line.svg +3 -0
  65. munchboka_edutools/static/icons/outline/dark_mode/file.svg +1 -0
  66. munchboka_edutools/static/icons/outline/dark_mode/fire.svg +4 -0
  67. munchboka_edutools/static/icons/outline/dark_mode/key.svg +3 -0
  68. munchboka_edutools/static/icons/outline/dark_mode/magnifying.svg +3 -0
  69. munchboka_edutools/static/icons/outline/dark_mode/pencil_square.svg +3 -0
  70. munchboka_edutools/static/icons/outline/dark_mode/play.svg +3 -0
  71. munchboka_edutools/static/icons/outline/dark_mode/question.svg +3 -0
  72. munchboka_edutools/static/icons/outline/dark_mode/square_check.svg +1 -0
  73. munchboka_edutools/static/icons/outline/dark_mode/stop.svg +3 -0
  74. munchboka_edutools/static/icons/outline/dark_mode/summary.svg +3 -0
  75. munchboka_edutools/static/icons/outline/dark_mode/undo.svg +3 -0
  76. munchboka_edutools/static/icons/outline/dark_mode/unlock.svg +3 -0
  77. munchboka_edutools/static/icons/outline/light_mode/academic.svg +3 -0
  78. munchboka_edutools/static/icons/outline/light_mode/backward.svg +3 -0
  79. munchboka_edutools/static/icons/outline/light_mode/book.svg +3 -0
  80. munchboka_edutools/static/icons/outline/light_mode/chat_bubble.svg +3 -0
  81. munchboka_edutools/static/icons/outline/light_mode/check.svg +3 -0
  82. munchboka_edutools/static/icons/outline/light_mode/cmd_line.svg +3 -0
  83. munchboka_edutools/static/icons/outline/light_mode/file.svg +1 -0
  84. munchboka_edutools/static/icons/outline/light_mode/fire.svg +4 -0
  85. munchboka_edutools/static/icons/outline/light_mode/key.svg +3 -0
  86. munchboka_edutools/static/icons/outline/light_mode/magnifying.svg +3 -0
  87. munchboka_edutools/static/icons/outline/light_mode/pencil_square.svg +3 -0
  88. munchboka_edutools/static/icons/outline/light_mode/play.svg +3 -0
  89. munchboka_edutools/static/icons/outline/light_mode/question.svg +3 -0
  90. munchboka_edutools/static/icons/outline/light_mode/square_check.svg +1 -0
  91. munchboka_edutools/static/icons/outline/light_mode/stop.svg +3 -0
  92. munchboka_edutools/static/icons/outline/light_mode/summary.svg +3 -0
  93. munchboka_edutools/static/icons/outline/light_mode/undo.svg +3 -0
  94. munchboka_edutools/static/icons/outline/light_mode/unlock.svg +3 -0
  95. munchboka_edutools/static/icons/polyicons/cubicdown.svg +3 -0
  96. munchboka_edutools/static/icons/polyicons/cubicup.svg +3 -0
  97. munchboka_edutools/static/icons/polyicons/frown.svg +3 -0
  98. munchboka_edutools/static/icons/polyicons/smile.svg +3 -0
  99. munchboka_edutools/static/icons/solid/dark_mode/academic.svg +5 -0
  100. munchboka_edutools/static/icons/solid/dark_mode/backward.svg +3 -0
  101. munchboka_edutools/static/icons/solid/dark_mode/book.svg +3 -0
  102. munchboka_edutools/static/icons/solid/dark_mode/brain.svg +1 -0
  103. munchboka_edutools/static/icons/solid/dark_mode/file.svg +1 -0
  104. munchboka_edutools/static/icons/solid/dark_mode/fire.svg +3 -0
  105. munchboka_edutools/static/icons/solid/dark_mode/key.svg +3 -0
  106. munchboka_edutools/static/icons/solid/dark_mode/pencil_square.svg +4 -0
  107. munchboka_edutools/static/icons/solid/dark_mode/play.svg +3 -0
  108. munchboka_edutools/static/icons/solid/dark_mode/python.svg +1 -0
  109. munchboka_edutools/static/icons/solid/dark_mode/scroll.svg +1 -0
  110. munchboka_edutools/static/icons/solid/dark_mode/stop.svg +3 -0
  111. munchboka_edutools/static/icons/solid/light_mode/academic.svg +5 -0
  112. munchboka_edutools/static/icons/solid/light_mode/backward.svg +3 -0
  113. munchboka_edutools/static/icons/solid/light_mode/book.svg +3 -0
  114. munchboka_edutools/static/icons/solid/light_mode/brain.svg +1 -0
  115. munchboka_edutools/static/icons/solid/light_mode/file.svg +1 -0
  116. munchboka_edutools/static/icons/solid/light_mode/fire.svg +3 -0
  117. munchboka_edutools/static/icons/solid/light_mode/key.svg +3 -0
  118. munchboka_edutools/static/icons/solid/light_mode/pencil_square.svg +4 -0
  119. munchboka_edutools/static/icons/solid/light_mode/play.svg +3 -0
  120. munchboka_edutools/static/icons/solid/light_mode/python.svg +1 -0
  121. munchboka_edutools/static/icons/solid/light_mode/scroll.svg +1 -0
  122. munchboka_edutools/static/icons/solid/light_mode/stop.svg +3 -0
  123. munchboka_edutools/static/icons/stickers/edit.svg +1 -0
  124. munchboka_edutools/static/icons/stickers/pencil_square.svg +3 -0
  125. munchboka_edutools/static/js/casThemeManager.js +99 -0
  126. munchboka_edutools/static/js/escapeRoom/escape-room.js +219 -0
  127. munchboka_edutools/static/js/geogebra-setup.js +6 -0
  128. munchboka_edutools/static/js/highlight-init.js +6 -0
  129. munchboka_edutools/static/js/interactiveCode/codeEditor.js +632 -0
  130. munchboka_edutools/static/js/interactiveCode/interactiveCodeSetup.js +348 -0
  131. munchboka_edutools/static/js/interactiveCode/pythonRunner.js +336 -0
  132. munchboka_edutools/static/js/interactiveCode/turtleCode.js +203 -0
  133. munchboka_edutools/static/js/interactiveCode/workerManager.js +353 -0
  134. munchboka_edutools/static/js/jeopardy.js +523 -0
  135. munchboka_edutools/static/js/pairPuzzle/draggableItem.js +64 -0
  136. munchboka_edutools/static/js/pairPuzzle/dropZone.js +119 -0
  137. munchboka_edutools/static/js/pairPuzzle/game.js +160 -0
  138. munchboka_edutools/static/js/parsons/parsonsPuzzle.js +641 -0
  139. munchboka_edutools/static/js/popup.js +85 -0
  140. munchboka_edutools/static/js/quiz.js +422 -0
  141. munchboka_edutools/static/js/skulpt/skulpt.js +35721 -0
  142. munchboka_edutools/static/js/timedQuiz/multipleChoiceQuestion.js +184 -0
  143. munchboka_edutools/static/js/timedQuiz/timedMultipleChoiceQuiz.js +244 -0
  144. munchboka_edutools/static/js/timedQuiz/utils.js +6 -0
  145. munchboka_edutools/static/js/utils.js +3 -0
  146. munchboka_edutools-0.1.13.dist-info/METADATA +108 -0
  147. munchboka_edutools-0.1.13.dist-info/RECORD +149 -0
  148. munchboka_edutools-0.1.13.dist-info/WHEEL +4 -0
  149. munchboka_edutools-0.1.13.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,632 @@
1
+
2
+
3
+
4
+ class CodeEditor {
5
+ constructor(editorId) {
6
+ this.editorId = editorId; // ID of the HTML textarea element to convert to a code editor
7
+ this.loadAddons().then(() => {
8
+ this.editor = this.initializeEditor(editorId); // Initialize the CodeMirror editor instance
9
+ this.editor = this.addCommentOverlay(this.editor); // Add custom overlay for highlighting comments
10
+ this.setupThemeListener(); // Set up a listener to detect and apply theme changes
11
+ this.refreshOnVisibilityChange(); // Add this line
12
+ });
13
+
14
+ // this.editor = this.initializeEditor(editorId); // Initialize the CodeMirror editor instance
15
+ // this.editor = this.addCommentOverlay(this.editor); // Add custom overlay for highlighting comments
16
+ // this.setupThemeListener(); // Set up a listener to detect and apply theme changes
17
+ // this.refreshOnVisibilityChange(); // Add this line
18
+ }
19
+
20
+ /**
21
+ * Initializes the CodeMirror editor with custom settings.
22
+ * @param {string} editorId - The ID of the textarea element to enhance with CodeMirror.
23
+ * @returns {Object} - The initialized CodeMirror editor instance.
24
+ */
25
+ initializeEditor(editorId) {
26
+ const self = this;
27
+ const editor = CodeMirror.fromTextArea(document.getElementById(editorId), {
28
+ mode: {
29
+ name: "python", // Language mode set to Python
30
+ version: 3, // Python 3 syntax
31
+ },
32
+ lineNumbers: true, // Enable line numbers
33
+ theme: this.getCurrentTheme(), // Set the initial theme based on user preference
34
+ tabSize: 4, // Set the tab size for indentation
35
+ indentUnit: 4, // Number of spaces per indentation level
36
+ matchBrackets: true, // Highlight matching brackets
37
+ autoCloseBrackets: true, // Automatically close brackets
38
+ extraKeys: {
39
+ // Tab navigates placeholders if a snippet is active; otherwise inserts spaces
40
+ Tab: (cm) => {
41
+ const st = cm.state && cm.state.snippetPH;
42
+ if (st && Array.isArray(st.markers) && st.markers.length) {
43
+ // Try to move to the next existing marker
44
+ let i = st.index + 1;
45
+ let moved = false;
46
+ while (i < st.markers.length) {
47
+ const pos = st.markers[i] && st.markers[i].find();
48
+ if (pos) {
49
+ st.index = i;
50
+ cm.setSelection(pos.from, pos.to);
51
+ moved = true;
52
+ break;
53
+ }
54
+ i++;
55
+ }
56
+ if (!moved) {
57
+ // No further placeholders: clear and fall back to normal Tab
58
+ this.clearSnippetPlaceholders(cm);
59
+ this.replaceTabWithSpaces(cm);
60
+ }
61
+ return; // handled
62
+ }
63
+ this.replaceTabWithSpaces(cm);
64
+ },
65
+ "Enter": function(cm) {
66
+ // If a completion popup is open, let it handle Enter
67
+ if (cm.state && cm.state.completionActive) {
68
+ return CodeMirror.Pass;
69
+ }
70
+ // Try snippet expansion first
71
+ if (self.tryExpandSnippet(cm)) return;
72
+
73
+ var cursor = cm.getCursor();
74
+ var line = cm.getLine(cursor.line);
75
+ var currentIndent = line.match(/^\s*/)[0]; // Get current indentation level
76
+
77
+
78
+ if (cursor.ch === 0) {
79
+ var currentLine = cm.getLine(cursor.line);
80
+ var currentIndent = currentLine.match(/^\s*/)[0];
81
+ cm.replaceSelection("\n" + currentIndent);
82
+ return;
83
+ }
84
+
85
+ // Rest of your existing logic
86
+ if (line.trim() === '') {
87
+ var nextLine = cursor.line < cm.lineCount() - 1 ? cm.getLine(cursor.line + 1) : "";
88
+ var nextIndent = nextLine ? nextLine.match(/^\s*/)[0] : "";
89
+
90
+ if (nextLine === "" || nextIndent.length < currentIndent.length) {
91
+ let reducedIndent = currentIndent.slice(0, Math.max(0, currentIndent.length - cm.getOption("indentUnit")));
92
+ cm.replaceSelection("\n" + reducedIndent);
93
+ } else {
94
+ cm.replaceSelection("\n" + currentIndent);
95
+ }
96
+ } else if (/:\s*$/.test(line)) {
97
+ cm.replaceSelection("\n" + currentIndent + Array(cm.getOption("indentUnit") + 1).join(" "));
98
+ } else {
99
+ cm.replaceSelection("\n" + currentIndent);
100
+ }
101
+ },
102
+ "Shift-Enter": function(cm) {
103
+ // Always create a new line with the same indentation as the current line
104
+ var cursor = cm.getCursor();
105
+ var line = cm.getLine(cursor.line);
106
+ var currentIndent = line.match(/^\s*/)[0];
107
+ cm.replaceSelection("\n" + currentIndent);
108
+ },
109
+ "Ctrl-.": "toggleComment", // For Windows/Linux
110
+ "Cmd-.": "toggleComment", // For Mac
111
+ "Ctrl-Space": "autocomplete",
112
+ // Shift-Tab navigates to previous placeholder when available
113
+ "Shift-Tab": (cm) => {
114
+ const st = cm.state && cm.state.snippetPH;
115
+ if (st && Array.isArray(st.markers) && st.markers.length) {
116
+ let i = st.index - 1;
117
+ while (i >= 0) {
118
+ const pos = st.markers[i] && st.markers[i].find();
119
+ if (pos) {
120
+ st.index = i;
121
+ cm.setSelection(pos.from, pos.to);
122
+ return;
123
+ }
124
+ i--;
125
+ }
126
+ // No previous valid marker; keep current selection as-is
127
+ }
128
+ },
129
+ // Placeholder navigation for snippets
130
+ "Ctrl-]": cm => this.selectNextPlaceholder(cm),
131
+ "Cmd-]": cm => this.selectNextPlaceholder(cm),
132
+ "Ctrl-[": cm => this.selectPrevPlaceholder(cm),
133
+ "Cmd-[": cm => this.selectPrevPlaceholder(cm),
134
+
135
+ "Backspace": function(cm) {
136
+ // Get cursor position
137
+ const cursor = cm.getCursor();
138
+ const line = cm.getLine(cursor.line);
139
+
140
+ // Check if we're at an empty line with indentation
141
+ if (line.trim() === '' && cursor.ch > 0 && cursor.ch % 4 === 0) {
142
+ // Delete 4 spaces (one indentation level)
143
+ cm.replaceRange("",
144
+ {line: cursor.line, ch: cursor.ch - 4},
145
+ {line: cursor.line, ch: cursor.ch});
146
+ } else {
147
+ // Normal backspace behavior
148
+ CodeMirror.commands.delCharBefore(cm);
149
+ }
150
+ },
151
+
152
+ },
153
+
154
+ });
155
+
156
+ // Show snippet hint popup as user types letters/underscore
157
+ editor.on('inputRead', function(cm, change) {
158
+ try {
159
+ if (!change || !change.text) return;
160
+ // Trigger on single-character word-like input
161
+ if (change.text.length === 1 && /^[A-Za-z_]$/.test(change.text[0])) {
162
+ self.triggerSnippetHint(cm);
163
+ }
164
+ } catch (e) {
165
+ // fail-safe
166
+ console.debug('snippet hint inputRead error', e);
167
+ }
168
+ });
169
+
170
+ return editor;
171
+ }
172
+
173
+
174
+ async loadAddons() {
175
+ // Only load addons if CodeMirror exists
176
+ if (typeof CodeMirror === 'undefined') {
177
+ console.error("CodeMirror not found! Addons will not be loaded.");
178
+ return;
179
+ }
180
+
181
+ const addons = [
182
+ // Core editing helpers
183
+ 'https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/addon/edit/matchbrackets.min.js',
184
+ 'https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/addon/edit/closebrackets.min.js',
185
+ 'https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/addon/comment/comment.min.js',
186
+ // Show-hint for autocomplete popup
187
+ 'https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/addon/hint/show-hint.min.js',
188
+ ];
189
+
190
+ const styles = [
191
+ 'https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/addon/hint/show-hint.min.css',
192
+ ];
193
+
194
+ const loadScript = (src) => {
195
+ return new Promise((resolve, reject) => {
196
+ const script = document.createElement('script');
197
+ script.src = src;
198
+ script.onload = resolve;
199
+ script.onerror = reject;
200
+ document.head.appendChild(script);
201
+ });
202
+ };
203
+
204
+ const loadStyle = (href) => {
205
+ return new Promise((resolve, reject) => {
206
+ // Avoid duplicate loads
207
+ if ([...document.styleSheets].some(s => s.href === href)) return resolve();
208
+ const link = document.createElement('link');
209
+ link.rel = 'stylesheet';
210
+ link.href = href;
211
+ link.onload = resolve;
212
+ link.onerror = reject;
213
+ document.head.appendChild(link);
214
+ });
215
+ };
216
+
217
+ for (const href of styles) {
218
+ try {
219
+ await loadStyle(href);
220
+ console.log(`Loaded CSS: ${href}`);
221
+ } catch (error) {
222
+ console.error(`Failed to load CSS: ${href}`, error);
223
+ }
224
+ }
225
+
226
+ for (const addon of addons) {
227
+ try {
228
+ await loadScript(addon);
229
+ console.log(`Loaded: ${addon}`);
230
+ } catch (error) {
231
+ console.error(`Failed to load: ${addon}`, error);
232
+ }
233
+ }
234
+ }
235
+
236
+ /**
237
+ * Replaces the tab key press with spaces for consistent indentation.
238
+ * @param {Object} cm - The CodeMirror instance.
239
+ */
240
+ replaceTabWithSpaces(cm) {
241
+ let spaces = Array(cm.getOption("indentUnit") + 1).join(" ");
242
+ cm.replaceSelection(spaces);
243
+ }
244
+
245
+ // ---------- Snippets ----------
246
+ // Simple snippet dictionary (keyword -> template and initial selection word)
247
+ getSnippets() {
248
+ return {
249
+ // Norwegian-friendly placeholders
250
+ "for": {
251
+ template: "for n in range(start, stopp, avstand):\n print(n)",
252
+ caretWords: ["start", "stopp", "avstand"]
253
+ },
254
+ "if": {
255
+ template: "if betingelse:\n pass",
256
+ caretWords: ["betingelse", "pass"]
257
+ },
258
+ "while": {
259
+ template: "while betingelse:\n pass",
260
+ caretWords: ["betingelse", "pass"]
261
+ },
262
+ "if-else": {
263
+ template: "if betingelse:\n pass\nelse:\n pass",
264
+ caretWords: ["betingelse"]
265
+ },
266
+ "funksjon": {
267
+ template: "def funksjonsnavn(variabel):\n return funksjonsuttrykk",
268
+ caretWords: ["funksjonsnavn", "variabel", "funksjonsuttrykk"]
269
+ },
270
+ "sum": {
271
+ template: "s = 0\nfor n in range(start, stopp, avstand):\n a = formel\n s = s + a",
272
+ caretWords: ["start", "stopp", "avstand", "formel"]
273
+ },
274
+ "print": {
275
+ template: "print(verdi)",
276
+ caretWords: ["verdi"]
277
+ },
278
+ "print-f": {
279
+ template: "print(f\"{variabel = }\")",
280
+ caretWords: ["variabel"]
281
+ },
282
+ "likning": {
283
+ template: "for x in range(start, stopp, avstand):\n if venstre_side == høyre_side:\n print(x)",
284
+ caretWords: ["start", "stopp", "avstand", "venstre_side", "høyre_side"]
285
+ },
286
+ };
287
+ }
288
+
289
+ // Expand snippet if the current line is exactly a keyword (ignoring leading spaces)
290
+ tryExpandSnippet(cm) {
291
+ const cur = cm.getCursor();
292
+ const lineText = cm.getLine(cur.line);
293
+ const leadingWSMatch = lineText.match(/^\s*/);
294
+ const leadingWS = leadingWSMatch ? leadingWSMatch[0] : "";
295
+ const trimmed = lineText.slice(leadingWS.length);
296
+
297
+ const snippets = this.getSnippets();
298
+ const snip = snippets[trimmed];
299
+ if (!snip) return false;
300
+
301
+ const { template, caretWords } = snip;
302
+
303
+ // Indent template according to current indentation and editor indent unit
304
+ const indented = this.buildIndentedTemplate(cm, leadingWS, template);
305
+
306
+ cm.operation(() => {
307
+ // Replace entire current line with the snippet to avoid duplicate indentation
308
+ cm.replaceRange(
309
+ indented,
310
+ { line: cur.line, ch: 0 },
311
+ { line: cur.line, ch: lineText.length }
312
+ );
313
+
314
+ // Prepare placeholders and select the first, if any
315
+ this.postInsertSnippet(cm, cur.line, indented, caretWords);
316
+ });
317
+
318
+ return true;
319
+ }
320
+
321
+ // Build an indented template that honors the editor's indentUnit.
322
+ // For each template line, count its leading whitespace (spaces/tabs), convert to indent levels,
323
+ // and prefix with the current line's indentation plus that many indent levels.
324
+ buildIndentedTemplate(cm, leadingWS, template) {
325
+ const unit = cm.getOption('indentUnit') || 4;
326
+ const indentStr = ' '.repeat(unit);
327
+ const toLevels = (ws) => {
328
+ let count = 0;
329
+ for (let i = 0; i < ws.length; i++) {
330
+ count += (ws[i] === '\t') ? unit : 1;
331
+ }
332
+ return Math.floor(count / unit);
333
+ };
334
+ const lines = template.split('\n');
335
+ return lines.map(line => {
336
+ const m = line.match(/^[ \t]*/)[0];
337
+ const level = toLevels(m);
338
+ const content = line.slice(m.length);
339
+ return leadingWS + indentStr.repeat(level) + content;
340
+ }).join('\n');
341
+ }
342
+
343
+ // Trigger snippet hint popup as the user types
344
+ triggerSnippetHint(cm) {
345
+ if (!CodeMirror || !CodeMirror.showHint) return;
346
+ CodeMirror.showHint(cm, (cm_) => this.snippetHint(cm_), { completeSingle: false });
347
+ }
348
+
349
+ // Custom hint provider that suggests snippet keywords and inserts templates
350
+ snippetHint(cm) {
351
+ const cur = cm.getCursor();
352
+ const lineText = cm.getLine(cur.line);
353
+ const leadingWSMatch = lineText.match(/^\s*/);
354
+ const leadingWS = leadingWSMatch ? leadingWSMatch[0] : "";
355
+
356
+ // Determine the current word before the cursor
357
+ let startCh = cur.ch;
358
+ while (startCh > 0 && /[A-Za-z_]/.test(lineText.charAt(startCh - 1))) startCh--;
359
+ const prefix = lineText.slice(startCh, cur.ch);
360
+
361
+ // Only offer snippet completions when typing at indentation start
362
+ if (startCh !== leadingWS.length) {
363
+ return { list: [], from: { line: cur.line, ch: cur.ch }, to: { line: cur.line, ch: cur.ch } };
364
+ }
365
+
366
+ const snippets = this.getSnippets();
367
+ const keys = Object.keys(snippets).filter(k => k.indexOf(prefix) === 0);
368
+
369
+ const list = keys.map(key => {
370
+ const { template } = snippets[key];
371
+ return {
372
+ text: key,
373
+ displayText: `${key} — ${template.split('\n')[0]}…`,
374
+ hint: (cmPick) => {
375
+ // Replace the current word with the snippet template, with indentation
376
+ const { template: tpl, caretWords } = snippets[key];
377
+ const indented = this.buildIndentedTemplate(cmPick, leadingWS, tpl);
378
+ cmPick.operation(() => {
379
+ // Replace from start of line to cursor to include indentation
380
+ cmPick.replaceRange(
381
+ indented,
382
+ { line: cur.line, ch: 0 },
383
+ { line: cur.line, ch: cur.ch }
384
+ );
385
+ // Prepare placeholders and select the first
386
+ this.postInsertSnippet(cmPick, cur.line, indented, caretWords);
387
+ });
388
+ }
389
+ };
390
+ });
391
+
392
+ return {
393
+ list,
394
+ from: { line: cur.line, ch: startCh },
395
+ to: { line: cur.line, ch: cur.ch }
396
+ };
397
+ }
398
+
399
+ // After inserting a snippet, create live markers for placeholders and select the first
400
+ postInsertSnippet(cm, baseLine, indented, caretWords) {
401
+ const words = Array.isArray(caretWords) ? caretWords : (caretWords ? [caretWords] : []);
402
+ const lines = indented.split('\n');
403
+ const markers = [];
404
+
405
+ if (words.length) {
406
+ for (let i = 0; i < lines.length; i++) {
407
+ for (const w of words) {
408
+ let idx = -1, fromCh = 0;
409
+ while ((idx = lines[i].indexOf(w, fromCh)) !== -1) {
410
+ const from = { line: baseLine + i, ch: idx };
411
+ const to = { line: baseLine + i, ch: idx + w.length };
412
+ const m = cm.getDoc().markText(from, to, { inclusiveLeft: true, inclusiveRight: true });
413
+ markers.push(m);
414
+ fromCh = idx + w.length;
415
+ }
416
+ }
417
+ }
418
+ }
419
+
420
+ cm.state.snippetPH = { markers, index: 0 };
421
+
422
+ // Helper to select current marker if it still exists
423
+ const selectIndex = (idx) => {
424
+ if (!markers.length) return false;
425
+ const mk = markers[idx];
426
+ if (!mk) return false;
427
+ const pos = mk.find();
428
+ if (!pos) return false; // marker cleared by edit
429
+ cm.setSelection(pos.from, pos.to);
430
+ return true;
431
+ };
432
+
433
+ if (!selectIndex(0)) {
434
+ // Fallback: put cursor at end of first inserted line
435
+ cm.setCursor({ line: baseLine, ch: lines[0].length });
436
+ }
437
+ }
438
+
439
+ // Move to the next existing placeholder marker
440
+ selectNextPlaceholder(cm) {
441
+ const st = cm.state && cm.state.snippetPH;
442
+ if (!st || !st.markers || !st.markers.length) return;
443
+ let i = st.index + 1;
444
+ while (i < st.markers.length) {
445
+ const pos = st.markers[i] && st.markers[i].find();
446
+ if (pos) {
447
+ st.index = i;
448
+ cm.setSelection(pos.from, pos.to);
449
+ return;
450
+ }
451
+ i++;
452
+ }
453
+ // No further placeholders: clear state
454
+ this.clearSnippetPlaceholders(cm);
455
+ }
456
+
457
+ // Move to the previous existing placeholder marker
458
+ selectPrevPlaceholder(cm) {
459
+ const st = cm.state && cm.state.snippetPH;
460
+ if (!st || !st.markers || !st.markers.length) return;
461
+ let i = st.index - 1;
462
+ while (i >= 0) {
463
+ const pos = st.markers[i] && st.markers[i].find();
464
+ if (pos) {
465
+ st.index = i;
466
+ cm.setSelection(pos.from, pos.to);
467
+ return;
468
+ }
469
+ i--;
470
+ }
471
+ // No previous: keep current or clear if current is gone
472
+ const cur = st.markers[st.index] && st.markers[st.index].find();
473
+ if (!cur) this.clearSnippetPlaceholders(cm);
474
+ }
475
+
476
+ clearSnippetPlaceholders(cm) {
477
+ const st = cm.state && cm.state.snippetPH;
478
+ if (!st) return;
479
+ if (st.markers) {
480
+ st.markers.forEach(m => { try { m.clear(); } catch(_){} });
481
+ }
482
+ delete cm.state.snippetPH;
483
+ }
484
+
485
+ /**
486
+ * Dynamically determines and applies the current theme based on user preference.
487
+ * @returns {string} - The theme name to be applied.
488
+ */
489
+ getCurrentTheme() {
490
+ const mode = document.documentElement.getAttribute('data-mode');
491
+ const lightTheme = "github-light";
492
+ const darkTheme = "github-dark-high-contrast";
493
+
494
+ if (mode === 'dark') {
495
+ return darkTheme;
496
+ } else if (mode === 'light') {
497
+ return lightTheme;
498
+ } else if (mode === 'auto') {
499
+ const prefersDarkScheme = window.matchMedia('(prefers-color-scheme: dark)').matches;
500
+ return prefersDarkScheme ? darkTheme : lightTheme;
501
+ }
502
+ }
503
+
504
+ /**
505
+ * Sets up a listener to dynamically update the theme when the user changes it.
506
+ */
507
+ setupThemeListener() {
508
+ const observer = new MutationObserver(mutations => {
509
+ mutations.forEach(mutation => {
510
+ if (mutation.attributeName === 'data-mode') {
511
+ this.editor.setOption('theme', this.getCurrentTheme());
512
+ }
513
+ });
514
+ });
515
+
516
+ observer.observe(document.documentElement, {
517
+ attributes: true,
518
+ attributeFilter: ['data-mode'],
519
+ });
520
+
521
+ // Set the initial theme when the editor is first initialized
522
+ this.editor.setOption('theme', this.getCurrentTheme());
523
+ }
524
+
525
+ /**
526
+ * Adds a custom overlay for highlighting specific comments for
527
+ * Supports # TODO, # FIKS MEG, # FYLL INN, # NOTE, # FIKSMEG, # IGNORER
528
+ * @returns {Object} - The overlay mode configuration for CodeMirror.
529
+ */
530
+ addCommentOverlay(editor) {
531
+ editor.addOverlay({
532
+ token: function(stream) {
533
+ const keywords = [
534
+ "# TODO",
535
+ "# FIKSMEG",
536
+ "# FIKS MEG",
537
+ "# NOTE",
538
+ "# FYLL INN",
539
+ "# IGNORER",
540
+ "# IKKE RØR",
541
+ "# FOKUS",
542
+ "# FORKLARING",
543
+ "# <--",
544
+ "# MERK",
545
+ "????",
546
+ ];
547
+
548
+ for (const keyword of keywords) {
549
+ if (stream.match(keyword)) {
550
+ // Special-case: map "# <--" to the same class as TODO
551
+ if (keyword === "# <--") {
552
+ return "todo";
553
+ }
554
+ return keyword
555
+ .replace("# ", "")
556
+ .toLowerCase()
557
+ .replace(" ", "")
558
+ .replace(/\?+/g, "question");
559
+ }
560
+ }
561
+
562
+ while (stream.next() != null && !keywords.some(keyword => stream.match(keyword, false))) {}
563
+ return null;
564
+ }
565
+ });
566
+ return editor;
567
+ }
568
+
569
+ /**
570
+ * Sets the code in the editor to a specified value.
571
+ * @param {string} code - The code to set in the editor.
572
+ */
573
+ setValue(code) {
574
+ this.editor.setValue(code);
575
+ }
576
+
577
+ /**
578
+ * Gets the current value (code) from the editor.
579
+ * @returns {string} - The code currently in the editor.
580
+ */
581
+ getValue() {
582
+ return this.editor.getValue();
583
+ }
584
+
585
+ /**
586
+ * Clears the editor content and optionally resets it to its initial value.
587
+ * @param {string} [initialValue=""] - The initial code value to reset the editor to (optional).
588
+ */
589
+ resetEditor(initialValue = "") {
590
+ this.editor.setValue(initialValue);
591
+ }
592
+
593
+ /**
594
+ * Highlights a specific line in the editor (useful for debugging or showing errors).
595
+ * @param {number} line - The line number to highlight (0-indexed).
596
+ */
597
+ highlightLine(line) {
598
+ console.log("Highlighting line", line);
599
+ this.editor.addLineClass(line, "background", "cm-highlight");
600
+ }
601
+
602
+ removehighlightLine(line) {
603
+ this.editor.removeLineClass(line, "background", "line-highlight-red");
604
+ }
605
+
606
+ clearLineHighlights() {
607
+ for (let i = 0; i < this.editor.lineCount(); i++) {
608
+ this.editor.removeLineClass(i, "background", "cm-highlight");
609
+ }
610
+ }
611
+
612
+ /**
613
+ * Scrolls the editor to a specified line, useful for showing errors or results.
614
+ * @param {number} line - The line number to scroll to (0-indexed).
615
+ */
616
+ scrollToLine(line) {
617
+ this.editor.scrollIntoView({ line: line, ch: 0 }, 200);
618
+ }
619
+
620
+ refreshOnVisibilityChange() {
621
+ const editorElement = this.editor.getWrapperElement();
622
+ const observer = new IntersectionObserver((entries) => {
623
+ entries.forEach(entry => {
624
+ if (entry.isIntersecting) {
625
+ this.editor.refresh();
626
+ }
627
+ });
628
+ }, { threshold: 0.1 });
629
+
630
+ observer.observe(editorElement);
631
+ }
632
+ }