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