@schukai/monster 4.23.6 → 4.24.1

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 (29) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/package.json +1 -1
  3. package/source/components/content/viewer/html.mjs +171 -0
  4. package/source/components/content/viewer/message.mjs +704 -0
  5. package/source/components/content/viewer/style/html.pcss +10 -0
  6. package/source/components/content/viewer/style/message.pcss +148 -0
  7. package/source/components/content/viewer/stylesheet/html.mjs +38 -0
  8. package/source/components/content/viewer/stylesheet/message.mjs +38 -0
  9. package/source/components/content/viewer.mjs +626 -522
  10. package/source/components/form/digits.mjs +0 -1
  11. package/source/components/form/select.mjs +2787 -2845
  12. package/source/components/form/style/select.pcss +0 -4
  13. package/source/components/form/stylesheet/select.mjs +14 -7
  14. package/source/components/form/util/floating-ui.mjs +2 -1
  15. package/source/components/layout/board.mjs +0 -5
  16. package/source/components/layout/panel.mjs +1 -1
  17. package/source/components/layout/popper.mjs +19 -10
  18. package/source/components/layout/tabs.mjs +17 -13
  19. package/source/components/navigation/table-of-content.mjs +0 -1
  20. package/source/components/tree-menu/style/tree-menu.pcss +1 -0
  21. package/source/components/tree-menu/stylesheet/tree-menu.mjs +1 -1
  22. package/source/dom/sanitize-html.mjs +54 -0
  23. package/source/monster.mjs +3 -0
  24. package/source/text/markdown-parser.mjs +253 -241
  25. package/source/types/version.mjs +1 -1
  26. package/test/cases/monster.mjs +1 -1
  27. package/test/web/import.js +1 -0
  28. package/test/web/test.html +2 -2
  29. package/test/web/tests.js +555 -149
@@ -20,245 +20,257 @@ export { MarkdownToHTML };
20
20
  * and task list items.
21
21
  */
22
22
  class MarkdownToHTML {
23
- constructor(markdown, options = {}) {
24
- this.markdown = markdown;
25
- this.tokens = [];
26
-
27
- this.options = {
28
- taskListDisabled: true,
29
- codeHighlightClassPrefix: 'language-',
30
- escapeHTML: true,
31
- ...options
32
- };
33
-
34
- this._taskId = 0; // For unique checkbox IDs in task lists
35
- }
36
-
37
- /**
38
- * Tokenizes the Markdown input into a structured array of tokens.
39
- */
40
- tokenize() {
41
- const lines = this.markdown.split('\n');
42
- let inCodeBlock = false;
43
- let codeBuffer = [];
44
- let listBuffer = null;
45
- let listType = null; // 'ul' or 'ol'
46
- let codeLang = '';
47
-
48
- for (const line of lines) {
49
- const trimmed = line.trim();
50
-
51
- // Detect start/end of fenced code blocks
52
- const codeFenceMatch = trimmed.match(/^```(\w+)?/);
53
- if (codeFenceMatch) {
54
- if (inCodeBlock) {
55
- this.tokens.push({
56
- type: 'code-block',
57
- content: codeBuffer.join('\n'),
58
- language: codeLang || null
59
- });
60
- codeBuffer = [];
61
- codeLang = '';
62
- inCodeBlock = false;
63
- } else {
64
- this._flushList(listBuffer);
65
- listBuffer = null;
66
- listType = null;
67
- codeLang = codeFenceMatch[1] || '';
68
- inCodeBlock = true;
69
- }
70
- continue;
71
- }
72
-
73
- if (inCodeBlock) {
74
- codeBuffer.push(line);
75
- continue;
76
- }
77
-
78
- // Heading (e.g. #, ##, ###, etc.)
79
- if (/^#{1,6}\s/.test(trimmed)) {
80
- this._flushList(listBuffer);
81
- listBuffer = null;
82
- listType = null;
83
- const level = trimmed.match(/^#+/)[0].length;
84
- this.tokens.push({ type: 'heading', level, content: trimmed.slice(level + 1).trim() });
85
- continue;
86
- }
87
-
88
- // Ordered list item (e.g. 1. Item)
89
- if (/^\d+\.\s+/.test(trimmed)) {
90
- if (listType && listType !== 'ol') {
91
- this._flushList(listBuffer);
92
- listBuffer = null;
93
- }
94
- listBuffer = listBuffer || { type: 'ordered-list', items: [] };
95
- listType = 'ol';
96
- listBuffer.items.push(trimmed.replace(/^\d+\.\s+/, ''));
97
- continue;
98
- }
99
-
100
- // Unordered list item or task list (e.g. - Item, - [x] Task)
101
- if (/^[-+*]\s+/.test(trimmed)) {
102
- if (listType && listType !== 'ul') {
103
- this._flushList(listBuffer);
104
- listBuffer = null;
105
- }
106
- listBuffer = listBuffer || { type: 'unordered-list', items: [] };
107
- listType = 'ul';
108
-
109
- const content = trimmed.replace(/^[-+*]\s+/, '');
110
- const taskMatch = content.match(/^\[( |x|X)]\s+(.*)/);
111
-
112
- if (taskMatch) {
113
- listBuffer.items.push({
114
- type: 'task',
115
- checked: taskMatch[1].toLowerCase() === 'x',
116
- content: taskMatch[2]
117
- });
118
- } else {
119
- listBuffer.items.push(content);
120
- }
121
- continue;
122
- }
123
-
124
- // Blank line
125
- if (trimmed === '') {
126
- this._flushList(listBuffer);
127
- listBuffer = null;
128
- listType = null;
129
- this.tokens.push({ type: 'blank' });
130
- continue;
131
- }
132
-
133
- // Plain paragraph
134
- this._flushList(listBuffer);
135
- listBuffer = null;
136
- listType = null;
137
- this.tokens.push({ type: 'paragraph', content: trimmed });
138
- }
139
-
140
- // Flush any remaining list or code buffer at EOF
141
- this._flushList(listBuffer);
142
- if (inCodeBlock && codeBuffer.length > 0) {
143
- this.tokens.push({ type: 'code-block', content: codeBuffer.join('\n'), language: codeLang || null });
144
- }
145
-
146
- return this.tokens;
147
- }
148
-
149
- /**
150
- * Pushes the current list buffer into the token stream if it's not empty.
151
- */
152
- _flushList(listBuffer) {
153
- if (listBuffer && Array.isArray(listBuffer.items) && listBuffer.items.length > 0) {
154
- this.tokens.push(listBuffer);
155
- }
156
- }
157
-
158
- /**
159
- * Escapes HTML characters to prevent injection.
160
- */
161
- static escapeHTML(text) {
162
- return text
163
- .replace(/&/g, '&')
164
- .replace(/</g, '&lt;')
165
- .replace(/>/g, '&gt;');
166
- }
167
-
168
- /**
169
- * Filters out invalid characters in code language names.
170
- */
171
- static safeCodeLang(lang) {
172
- return typeof lang === 'string'
173
- ? lang.replace(/[^a-zA-Z0-9\-_]/g, '').slice(0, 32)
174
- : '';
175
- }
176
-
177
- /**
178
- * Converts inline Markdown to HTML (bold, italic, code, links).
179
- * Optionally escapes HTML.
180
- */
181
- parseInline(text) {
182
- let out = this.options.escapeHTML ? MarkdownToHTML.escapeHTML(text) : text;
183
-
184
- // Markdown elements
185
- out = out.replace(/\[([^\]]+)]\(([^)]+)\)/g, '<a href="$2">$1</a>'); // Links
186
- out = out.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>'); // Bold
187
- out = out.replace(/\*(.+?)\*/g, '<em>$1</em>'); // Italic
188
- out = out.replace(/`(.+?)`/g, '<code>$1</code>'); // Inline code
189
- return out;
190
- }
191
-
192
- /**
193
- * Converts tokens to final HTML.
194
- */
195
- render() {
196
- if (this.tokens.length === 0) {
197
- this.tokenize();
198
- }
199
-
200
- let html = '';
201
-
202
- for (const token of this.tokens) {
203
- switch (token.type) {
204
- case 'heading':
205
- html += `<h${token.level}>${this.parseInline(token.content)}</h${token.level}>\n`;
206
- break;
207
-
208
- case 'paragraph':
209
- html += `<p>${this.parseInline(token.content)}</p>\n`;
210
- break;
211
-
212
- case 'unordered-list':
213
- html += '<ul>\n';
214
- for (const item of token.items) {
215
- if (typeof item === 'string') {
216
- html += ` <li>${this.parseInline(item)}</li>\n`;
217
- } else if (item && item.type === 'task') {
218
- this._taskId += 1;
219
- const inputId = `mdtask-${this._taskId}`;
220
- const checked = item.checked ? ' checked' : '';
221
- const disabled = this.options.taskListDisabled ? ' disabled' : '';
222
- html += ` <li><input type="checkbox" id="${inputId}"${disabled}${checked}><label for="${inputId}"> ${this.parseInline(item.content)}</label></li>\n`;
223
- }
224
- }
225
- html += '</ul>\n';
226
- break;
227
-
228
- case 'ordered-list':
229
- html += '<ol>\n';
230
- for (const item of token.items) {
231
- html += ` <li>${this.parseInline(item)}</li>\n`;
232
- }
233
- html += '</ol>\n';
234
- break;
235
-
236
- case 'code-block': {
237
- const safeLang = MarkdownToHTML.safeCodeLang(token.language);
238
- const langClass = safeLang
239
- ? ` class="${this.options.codeHighlightClassPrefix}${safeLang}"`
240
- : '';
241
- const codeContent = this.options.escapeHTML
242
- ? MarkdownToHTML.escapeHTML(token.content)
243
- : token.content;
244
- html += `<pre><code${langClass}>${codeContent}</code></pre>\n`;
245
- break;
246
- }
247
-
248
- case 'blank':
249
- html += '\n';
250
- break;
251
- }
252
- }
253
-
254
- return html.trim();
255
- }
256
-
257
- /**
258
- * Convenience static method to render Markdown directly.
259
- */
260
- static convert(markdown, options = {}) {
261
- const converter = new MarkdownToHTML(markdown, options);
262
- return converter.render();
263
- }
23
+ constructor(markdown, options = {}) {
24
+ this.markdown = markdown;
25
+ this.tokens = [];
26
+
27
+ this.options = {
28
+ taskListDisabled: true,
29
+ codeHighlightClassPrefix: "language-",
30
+ escapeHTML: true,
31
+ ...options,
32
+ };
33
+
34
+ this._taskId = 0; // For unique checkbox IDs in task lists
35
+ }
36
+
37
+ /**
38
+ * Tokenizes the Markdown input into a structured array of tokens.
39
+ */
40
+ tokenize() {
41
+ const lines = this.markdown.split("\n");
42
+ let inCodeBlock = false;
43
+ let codeBuffer = [];
44
+ let listBuffer = null;
45
+ let listType = null; // 'ul' or 'ol'
46
+ let codeLang = "";
47
+
48
+ for (const line of lines) {
49
+ const trimmed = line.trim();
50
+
51
+ // Detect start/end of fenced code blocks
52
+ const codeFenceMatch = trimmed.match(/^```(\w+)?/);
53
+ if (codeFenceMatch) {
54
+ if (inCodeBlock) {
55
+ this.tokens.push({
56
+ type: "code-block",
57
+ content: codeBuffer.join("\n"),
58
+ language: codeLang || null,
59
+ });
60
+ codeBuffer = [];
61
+ codeLang = "";
62
+ inCodeBlock = false;
63
+ } else {
64
+ this._flushList(listBuffer);
65
+ listBuffer = null;
66
+ listType = null;
67
+ codeLang = codeFenceMatch[1] || "";
68
+ inCodeBlock = true;
69
+ }
70
+ continue;
71
+ }
72
+
73
+ if (inCodeBlock) {
74
+ codeBuffer.push(line);
75
+ continue;
76
+ }
77
+
78
+ // Heading (e.g. #, ##, ###, etc.)
79
+ if (/^#{1,6}\s/.test(trimmed)) {
80
+ this._flushList(listBuffer);
81
+ listBuffer = null;
82
+ listType = null;
83
+ const level = trimmed.match(/^#+/)[0].length;
84
+ this.tokens.push({
85
+ type: "heading",
86
+ level,
87
+ content: trimmed.slice(level + 1).trim(),
88
+ });
89
+ continue;
90
+ }
91
+
92
+ // Ordered list item (e.g. 1. Item)
93
+ if (/^\d+\.\s+/.test(trimmed)) {
94
+ if (listType && listType !== "ol") {
95
+ this._flushList(listBuffer);
96
+ listBuffer = null;
97
+ }
98
+ listBuffer = listBuffer || { type: "ordered-list", items: [] };
99
+ listType = "ol";
100
+ listBuffer.items.push(trimmed.replace(/^\d+\.\s+/, ""));
101
+ continue;
102
+ }
103
+
104
+ // Unordered list item or task list (e.g. - Item, - [x] Task)
105
+ if (/^[-+*]\s+/.test(trimmed)) {
106
+ if (listType && listType !== "ul") {
107
+ this._flushList(listBuffer);
108
+ listBuffer = null;
109
+ }
110
+ listBuffer = listBuffer || { type: "unordered-list", items: [] };
111
+ listType = "ul";
112
+
113
+ const content = trimmed.replace(/^[-+*]\s+/, "");
114
+ const taskMatch = content.match(/^\[( |x|X)]\s+(.*)/);
115
+
116
+ if (taskMatch) {
117
+ listBuffer.items.push({
118
+ type: "task",
119
+ checked: taskMatch[1].toLowerCase() === "x",
120
+ content: taskMatch[2],
121
+ });
122
+ } else {
123
+ listBuffer.items.push(content);
124
+ }
125
+ continue;
126
+ }
127
+
128
+ // Blank line
129
+ if (trimmed === "") {
130
+ this._flushList(listBuffer);
131
+ listBuffer = null;
132
+ listType = null;
133
+ this.tokens.push({ type: "blank" });
134
+ continue;
135
+ }
136
+
137
+ // Plain paragraph
138
+ this._flushList(listBuffer);
139
+ listBuffer = null;
140
+ listType = null;
141
+ this.tokens.push({ type: "paragraph", content: trimmed });
142
+ }
143
+
144
+ // Flush any remaining list or code buffer at EOF
145
+ this._flushList(listBuffer);
146
+ if (inCodeBlock && codeBuffer.length > 0) {
147
+ this.tokens.push({
148
+ type: "code-block",
149
+ content: codeBuffer.join("\n"),
150
+ language: codeLang || null,
151
+ });
152
+ }
153
+
154
+ return this.tokens;
155
+ }
156
+
157
+ /**
158
+ * Pushes the current list buffer into the token stream if it's not empty.
159
+ */
160
+ _flushList(listBuffer) {
161
+ if (
162
+ listBuffer &&
163
+ Array.isArray(listBuffer.items) &&
164
+ listBuffer.items.length > 0
165
+ ) {
166
+ this.tokens.push(listBuffer);
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Escapes HTML characters to prevent injection.
172
+ */
173
+ static escapeHTML(text) {
174
+ return text
175
+ .replace(/&/g, "&amp;")
176
+ .replace(/</g, "&lt;")
177
+ .replace(/>/g, "&gt;");
178
+ }
179
+
180
+ /**
181
+ * Filters out invalid characters in code language names.
182
+ */
183
+ static safeCodeLang(lang) {
184
+ return typeof lang === "string"
185
+ ? lang.replace(/[^a-zA-Z0-9\-_]/g, "").slice(0, 32)
186
+ : "";
187
+ }
188
+
189
+ /**
190
+ * Converts inline Markdown to HTML (bold, italic, code, links).
191
+ * Optionally escapes HTML.
192
+ */
193
+ parseInline(text) {
194
+ let out = this.options.escapeHTML ? MarkdownToHTML.escapeHTML(text) : text;
195
+
196
+ // Markdown elements
197
+ out = out.replace(/\[([^\]]+)]\(([^)]+)\)/g, '<a href="$2">$1</a>'); // Links
198
+ out = out.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>"); // Bold
199
+ out = out.replace(/\*(.+?)\*/g, "<em>$1</em>"); // Italic
200
+ out = out.replace(/`(.+?)`/g, "<code>$1</code>"); // Inline code
201
+ return out;
202
+ }
203
+
204
+ /**
205
+ * Converts tokens to final HTML.
206
+ */
207
+ render() {
208
+ if (this.tokens.length === 0) {
209
+ this.tokenize();
210
+ }
211
+
212
+ let html = "";
213
+
214
+ for (const token of this.tokens) {
215
+ switch (token.type) {
216
+ case "heading":
217
+ html += `<h${token.level}>${this.parseInline(token.content)}</h${token.level}>\n`;
218
+ break;
219
+
220
+ case "paragraph":
221
+ html += `<p>${this.parseInline(token.content)}</p>\n`;
222
+ break;
223
+
224
+ case "unordered-list":
225
+ html += "<ul>\n";
226
+ for (const item of token.items) {
227
+ if (typeof item === "string") {
228
+ html += ` <li>${this.parseInline(item)}</li>\n`;
229
+ } else if (item && item.type === "task") {
230
+ this._taskId += 1;
231
+ const inputId = `mdtask-${this._taskId}`;
232
+ const checked = item.checked ? " checked" : "";
233
+ const disabled = this.options.taskListDisabled ? " disabled" : "";
234
+ html += ` <li><input type="checkbox" id="${inputId}"${disabled}${checked}><label for="${inputId}"> ${this.parseInline(item.content)}</label></li>\n`;
235
+ }
236
+ }
237
+ html += "</ul>\n";
238
+ break;
239
+
240
+ case "ordered-list":
241
+ html += "<ol>\n";
242
+ for (const item of token.items) {
243
+ html += ` <li>${this.parseInline(item)}</li>\n`;
244
+ }
245
+ html += "</ol>\n";
246
+ break;
247
+
248
+ case "code-block": {
249
+ const safeLang = MarkdownToHTML.safeCodeLang(token.language);
250
+ const langClass = safeLang
251
+ ? ` class="${this.options.codeHighlightClassPrefix}${safeLang}"`
252
+ : "";
253
+ const codeContent = this.options.escapeHTML
254
+ ? MarkdownToHTML.escapeHTML(token.content)
255
+ : token.content;
256
+ html += `<pre><code${langClass}>${codeContent}</code></pre>\n`;
257
+ break;
258
+ }
259
+
260
+ case "blank":
261
+ html += "\n";
262
+ break;
263
+ }
264
+ }
265
+
266
+ return html.trim();
267
+ }
268
+
269
+ /**
270
+ * Convenience static method to render Markdown directly.
271
+ */
272
+ static convert(markdown, options = {}) {
273
+ const converter = new MarkdownToHTML(markdown, options);
274
+ return converter.render();
275
+ }
264
276
  }
@@ -156,7 +156,7 @@ function getMonsterVersion() {
156
156
  }
157
157
 
158
158
  /** don't touch, replaced by make with package.json version */
159
- monsterVersion = new Version("4.13.0");
159
+ monsterVersion = new Version("4.23.6");
160
160
 
161
161
  return monsterVersion;
162
162
  }
@@ -7,7 +7,7 @@ describe('Monster', function () {
7
7
  let monsterVersion
8
8
 
9
9
  /** don´t touch, replaced by make with package.json version */
10
- monsterVersion = new Version("4.13.0")
10
+ monsterVersion = new Version("4.23.6")
11
11
 
12
12
  let m = getMonsterVersion();
13
13
 
@@ -21,6 +21,7 @@ import "../cases/components/host/details.mjs";
21
21
  import "../cases/text/formatter.mjs";
22
22
  import "../cases/text/generate-range-comparison-expression.mjs";
23
23
  import "../cases/text/util.mjs";
24
+ import "../cases/text/markdown-parser.mjs";
24
25
  import "../cases/text/bracketed-key-value-hash.mjs";
25
26
  import "../cases/math/random.mjs";
26
27
  import "../cases/util/trimspaces.mjs";
@@ -9,8 +9,8 @@
9
9
  </head>
10
10
  <body>
11
11
  <div id="headline" style="display: flex;align-items: center;justify-content: center;flex-direction: column;">
12
- <h1 style='margin-bottom: 0.1em;'>Monster 4.13.0</h1>
13
- <div id="lastupdate" style='font-size:0.7em'>last update Di 3. Jun 20:49:21 CEST 2025</div>
12
+ <h1 style='margin-bottom: 0.1em;'>Monster 4.23.6</h1>
13
+ <div id="lastupdate" style='font-size:0.7em'>last update Mi 25. Jun 11:28:48 CEST 2025</div>
14
14
  </div>
15
15
  <div id="mocha-errors"
16
16
  style="color: red;font-weight: bold;display: flex;align-items: center;justify-content: center;flex-direction: column;margin:20px;"></div>