samengine 1.9.0 → 1.10.0

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 (87) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +203 -0
  3. package/dist/config/buildconfig.d.ts +146 -0
  4. package/dist/config/buildconfig.js +115 -0
  5. package/dist/config/index.d.ts +9 -0
  6. package/dist/config/index.js +1 -0
  7. package/dist/core.d.ts +17 -0
  8. package/dist/core.js +24 -0
  9. package/dist/html.d.ts +29 -0
  10. package/dist/html.js +20 -0
  11. package/dist/index.d.ts +2 -1
  12. package/dist/index.js +1 -2
  13. package/dist/input.d.ts +51 -0
  14. package/dist/input.js +44 -3
  15. package/dist/keys.d.ts +6 -0
  16. package/dist/keys.js +6 -2
  17. package/dist/logger.d.ts +8 -0
  18. package/dist/logger.js +8 -1
  19. package/dist/nonbrowser/getversion.d.ts +13 -0
  20. package/dist/nonbrowser/getversion.js +35 -0
  21. package/dist/nonbrowser/ghresolver.d.ts +1 -0
  22. package/dist/nonbrowser/ghresolver.js +7 -0
  23. package/dist/nonbrowser/index.d.ts +9 -0
  24. package/dist/nonbrowser/index.js +9 -0
  25. package/dist/nonbrowser/internal/buildhelper.d.ts +42 -0
  26. package/dist/nonbrowser/internal/buildhelper.js +144 -0
  27. package/dist/nonbrowser/internal/cli/argparser.d.ts +20 -0
  28. package/dist/nonbrowser/internal/cli/argparser.js +36 -0
  29. package/dist/nonbrowser/internal/cli/main.d.ts +13 -0
  30. package/dist/nonbrowser/internal/cli/main.js +262 -0
  31. package/dist/nonbrowser/internal/config.d.ts +9 -0
  32. package/dist/nonbrowser/internal/config.js +40 -0
  33. package/dist/nonbrowser/internal/exporthtml.d.ts +37 -0
  34. package/dist/nonbrowser/internal/exporthtml.js +622 -0
  35. package/dist/nonbrowser/internal/projcreator/downloadZip.d.ts +4 -0
  36. package/dist/nonbrowser/internal/projcreator/downloadZip.js +83 -0
  37. package/dist/nonbrowser/internal/projcreator/main.d.ts +1 -0
  38. package/dist/nonbrowser/internal/projcreator/main.js +81 -0
  39. package/dist/nonbrowser/utils.d.ts +8 -0
  40. package/dist/nonbrowser/utils.js +18 -0
  41. package/dist/physics/collision.d.ts +33 -0
  42. package/dist/physics/collision.js +27 -0
  43. package/dist/physics/physicsEngine.d.ts +18 -0
  44. package/dist/physics/physicsEngine.js +18 -0
  45. package/dist/physics/physicsObject.d.ts +20 -0
  46. package/dist/physics/physicsObject.js +20 -0
  47. package/dist/renderer.d.ts +85 -2
  48. package/dist/renderer.js +86 -7
  49. package/dist/samegui/index.d.ts +49 -0
  50. package/dist/samegui/index.js +137 -0
  51. package/dist/save.d.ts +30 -0
  52. package/dist/save.js +25 -0
  53. package/dist/sound/audioplayer.d.ts +39 -0
  54. package/dist/sound/audioplayer.js +39 -5
  55. package/dist/storage/index.d.ts +57 -0
  56. package/dist/storage/index.js +89 -0
  57. package/dist/text/index.d.ts +14 -0
  58. package/dist/text/index.js +58 -0
  59. package/dist/texture.d.ts +100 -0
  60. package/dist/texture.js +75 -41
  61. package/dist/types/button.d.ts +25 -0
  62. package/dist/types/button.js +22 -0
  63. package/dist/types/circle.d.ts +26 -0
  64. package/dist/types/circle.js +21 -7
  65. package/dist/types/color.d.ts +17 -0
  66. package/dist/types/color.js +11 -1
  67. package/dist/types/index.d.ts +1 -1
  68. package/dist/types/index.js +1 -1
  69. package/dist/types/rectangle.d.ts +29 -0
  70. package/dist/types/rectangle.js +23 -6
  71. package/dist/types/triangle.d.ts +23 -0
  72. package/dist/types/triangle.js +20 -6
  73. package/dist/types/vector2d.d.ts +42 -0
  74. package/dist/types/vector2d.js +39 -11
  75. package/dist/types/vector3d.d.ts +38 -0
  76. package/dist/types/vector3d.js +35 -11
  77. package/dist/utils/index.d.ts +13 -4
  78. package/dist/utils/index.js +26 -2
  79. package/dist/utils/logger/index.d.ts +24 -0
  80. package/dist/utils/logger/index.js +44 -0
  81. package/dist/utils/math.d.ts +18 -0
  82. package/dist/utils/math.js +18 -4
  83. package/package.json +40 -10
  84. package/dist/utils/jsonc-parser.d.ts +0 -4
  85. package/dist/utils/jsonc-parser.js +0 -166
  86. package/dist/utils/markdown.d.ts +0 -41
  87. package/dist/utils/markdown.js +0 -699
@@ -1,699 +0,0 @@
1
- // a Markdown Parser
2
- // ---------------------------------------------------------------------------
3
- // Hilfsfunktionen
4
- // ---------------------------------------------------------------------------
5
- function escapeHtml(text) {
6
- return text
7
- .replace(/&/g, "&")
8
- .replace(/</g, "&lt;")
9
- .replace(/>/g, "&gt;")
10
- .replace(/"/g, "&quot;")
11
- .replace(/'/g, "&#39;");
12
- }
13
- function unescapeHtml(text) {
14
- return text
15
- .replace(/&amp;/g, "&")
16
- .replace(/&lt;/g, "<")
17
- .replace(/&gt;/g, ">")
18
- .replace(/&quot;/g, '"')
19
- .replace(/&#39;/g, "'");
20
- }
21
- function smartypants(text) {
22
- return text
23
- .replace(/---/g, "\u2014") // Em-Dash
24
- .replace(/--/g, "\u2013") // En-Dash
25
- .replace(/\.{3}/g, "\u2026") // Ellipsis
26
- .replace(/"([^"]+)"/g, "\u201C$1\u201D")
27
- .replace(/'([^']+)'/g, "\u2018$1\u2019");
28
- }
29
- function isExternalUrl(url) {
30
- return /^https?:\/\//i.test(url);
31
- }
32
- // ---------------------------------------------------------------------------
33
- // Inline-Renderer
34
- // ---------------------------------------------------------------------------
35
- function renderInline(text, opts) {
36
- // ---------------------------------------------------------------------
37
- // Escaped Zeichen sichern (wichtig für Markdown-Sonderzeichen)
38
- // ---------------------------------------------------------------------
39
- const ESCAPES = {
40
- "\\\\": "@@ESC-BACKSLASH@@",
41
- "\\[": "@@ESC-LBRACKET@@",
42
- "\\]": "@@ESC-RBRACKET@@",
43
- "\\(": "@@ESC-LPAREN@@",
44
- "\\)": "@@ESC-RPAREN@@",
45
- "\\|": "@@ESC-PIPE@@",
46
- '\\"': "@@ESC-QUOTE@@",
47
- "\\*": "@@ESC-STAR@@",
48
- "\\_": "@@ESC-UNDERSCORE@@",
49
- "\\`": "@@ESC-BACKTICK@@",
50
- "\\~": "@@ESC-TILDE@@"
51
- };
52
- for (const [char, placeholder] of Object.entries(ESCAPES)) {
53
- const regex = new RegExp(char.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&"), "g");
54
- text = text.replace(regex, placeholder);
55
- }
56
- // Roh-HTML bewahren (falls !sanitize)
57
- const htmlPlaceholders = [];
58
- if (!opts.sanitize) {
59
- text = text.replace(/<[^>]+>/g, (match) => {
60
- const idx = htmlPlaceholders.push(match) - 1;
61
- return `@@HTML${idx}@@`;
62
- });
63
- }
64
- // Links [text](url "title")
65
- text = text.replace(/\[([^\]]+)\]\(([^)]+?)(?:\s+"([^"]*)")?\)/g, (_, linkText, url, title) => {
66
- const t = title ? ` title="${escapeHtml(title)}"` : "";
67
- const ext = opts.externalLinks && isExternalUrl(url)
68
- ? ' target="_blank" rel="noopener noreferrer"'
69
- : "";
70
- return `<a href="${url}"${t}${ext}>${linkText}</a>`;
71
- });
72
- // Code-Spans (höchste Priorität, vor allem anderen)
73
- text = text.replace(/`{2}([^`]+)`{2}|`([^`\n]+)`/g, (_, a, b) => {
74
- return `<code>${escapeHtml(a ?? b)}</code>`;
75
- });
76
- // Bilder ![alt](url "title")
77
- text = text.replace(/!\[([^\]]*)\]\(([^)]+?)(?:\s+"([^"]*)")?\)/g, (_, alt, url, title) => {
78
- const t = title ? ` title="${escapeHtml(title)}"` : "";
79
- return `<img src="${url}" alt="${escapeHtml(alt)}"${t}>`;
80
- });
81
- // Autolinks <https://…>
82
- text = text.replace(/<(https?:\/\/[^\s>]+)>/g, (_, url) => {
83
- const ext = opts.externalLinks
84
- ? ' target="_blank" rel="noopener noreferrer"'
85
- : "";
86
- return `<a href="${url}"${ext}>${url}</a>`;
87
- });
88
- // E-Mail-Autolinks <email@example.com>
89
- text = text.replace(/<([^@\s>]+@[^@\s>]+\.[^@\s>]+)>/g, (_, email) => {
90
- return `<a href="mailto:${email}">${email}</a>`;
91
- });
92
- // Fett + Kursiv ***text*** ___text___
93
- text = text.replace(/(\*{3}|_{3})(.+?)\1/g, "<strong><em>$2</em></strong>");
94
- // Fett **text** __text__
95
- text = text.replace(/(\*{2}|_{2})(.+?)\1/g, "<strong>$2</strong>");
96
- // Kursiv *text* _text_
97
- text = text.replace(/(\*|_)(.+?)\1/g, "<em>$2</em>");
98
- // Durchgestrichen ~~text~~
99
- text = text.replace(/~~(.+?)~~/g, "<del>$1</del>");
100
- // Hochgestellt ^text^
101
- text = text.replace(/\^([^^]+)\^/g, "<sup>$1</sup>");
102
- // Tiefgestellt ~text~ (nur wenn nicht ~~)
103
- text = text.replace(/(?<!~)~(?!~)([^~]+)~(?!~)/g, "<sub>$1</sub>");
104
- // Markiert ==text==
105
- text = text.replace(/==(.+?)==/g, "<mark>$1</mark>");
106
- // Zeilenumbrüche: zwei Leerzeichen + \n → <br>
107
- text = text.replace(/ {2,}\n/g, "<br>\n");
108
- // Harte Zeilenumbrüche (wenn breaks: true)
109
- if (opts.breaks) {
110
- text = text.replace(/\n/g, "<br>\n");
111
- }
112
- // Smartypants
113
- if (opts.smartypants) {
114
- text = smartypants(text);
115
- }
116
- // HTML-Platzhalter wiederherstellen
117
- if (!opts.sanitize) {
118
- text = text.replace(/@@HTML(\d+)@@/g, (_, i) => htmlPlaceholders[+i]);
119
- }
120
- text = text.replace(/@@ESC-BACKSLASH@@/g, "\\");
121
- text = text.replace(/@@ESC-LPAREN@@/g, "(");
122
- text = text.replace(/@@ESC-RPAREN@@/g, ")");
123
- text = text.replace(/@@ESC-PIPE@@/g, "|");
124
- text = text.replace(/@@ESC-QUOTE@@/g, '"');
125
- text = text.replace(/@@ESC-STAR@@/g, "*");
126
- text = text.replace(/@@ESC-UNDERSCORE@@/g, "_");
127
- text = text.replace(/@@ESC-BACKTICK@@/g, "`");
128
- text = text.replace(/@@ESC-TILDE@@/g, "~");
129
- text = text.replace(/@@ESC-RBRACKET@@/g, "]");
130
- text = text.replace(/@@ESC-LBRACKET@@/g, "[");
131
- return text;
132
- }
133
- function parseListItems(lines, baseIndent) {
134
- const items = [];
135
- let i = 0;
136
- while (i < lines.length) {
137
- const line = lines[i];
138
- const indentMatch = line.match(/^(\s*)/);
139
- const indent = indentMatch ? indentMatch[1].length : 0;
140
- if (indent < baseIndent)
141
- break;
142
- if (indent > baseIndent) {
143
- i++;
144
- continue;
145
- }
146
- const bulletMatch = line.match(/^\s*(?:[-*+]|\d+\.)\s+(.*)/);
147
- if (!bulletMatch) {
148
- i++;
149
- continue;
150
- }
151
- let itemText = bulletMatch[1];
152
- let task = false;
153
- let checked = false;
154
- // Aufgabenliste
155
- const taskMatch = itemText.match(/^\[([ xX])\]\s+(.*)/);
156
- if (taskMatch) {
157
- task = true;
158
- checked = taskMatch[1].toLowerCase() === "x";
159
- itemText = taskMatch[2];
160
- }
161
- // Untergeordnete Zeilen sammeln
162
- const childLines = [];
163
- i++;
164
- while (i < lines.length) {
165
- const nextIndent = (lines[i].match(/^(\s*)/) ?? ["", ""])[1].length;
166
- if (nextIndent <= baseIndent && lines[i].match(/^\s*(?:[-*+]|\d+\.)\s/))
167
- break;
168
- childLines.push(lines[i]);
169
- i++;
170
- }
171
- const children = childLines.length > 0
172
- ? parseListItems(childLines, baseIndent + 2)
173
- : [];
174
- items.push({ text: itemText, task, checked, children });
175
- }
176
- return items;
177
- }
178
- function renderListItems(items, ordered, opts) {
179
- return items
180
- .map((item) => {
181
- const checkbox = item.task
182
- ? `<input type="checkbox"${item.checked ? " checked" : ""} disabled> `
183
- : "";
184
- const childList = item.children.length > 0
185
- ? renderList(item.children, ordered, opts)
186
- : "";
187
- return `<li>${checkbox}${renderInline(item.text, opts)}${childList}</li>`;
188
- })
189
- .join("\n");
190
- }
191
- function renderList(items, ordered, opts) {
192
- const tag = ordered ? "ol" : "ul";
193
- return `<${tag}>\n${renderListItems(items, ordered, opts)}\n</${tag}>`;
194
- }
195
- // ---------------------------------------------------------------------------
196
- // Tabellen
197
- // ---------------------------------------------------------------------------
198
- function parseTable(block, opts) {
199
- const rows = block.trim().split("\n");
200
- if (rows.length < 2)
201
- return `<p>${renderInline(block, opts)}</p>`;
202
- const headerCells = rows[0]
203
- .split(/(?<!\\)\|/)
204
- .filter((_, i, a) => !(i === 0 && _ === "") && !(i === a.length - 1 && _ === ""))
205
- .map((c) => c
206
- .trim()
207
- // .replace(/\\\|/g, "|")
208
- // .replace(/\\\[/g, "[")
209
- // .replace(/\\\]/g, "]")
210
- );
211
- const alignRow = rows[1].split(/(?<!\\)\|/).filter((c) => /[-:]/.test(c));
212
- const aligns = alignRow.map((c) => {
213
- c = c.trim();
214
- if (c.startsWith(":") && c.endsWith(":"))
215
- return "center";
216
- if (c.endsWith(":"))
217
- return "right";
218
- if (c.startsWith(":"))
219
- return "left";
220
- return "";
221
- });
222
- const thead = `<thead>\n<tr>\n${headerCells
223
- .map((c, i) => {
224
- const align = aligns[i] ? ` style="text-align:${aligns[i]}"` : "";
225
- return `<th${align}>${renderInline(c, opts)}</th>`;
226
- })
227
- .join("\n")}\n</tr>\n</thead>`;
228
- const bodyRows = rows.slice(2).map((row) => {
229
- const cells = row
230
- .split(/(?<!\\)\|/)
231
- .filter((_, i, a) => !(i === 0 && _ === "") && !(i === a.length - 1 && _ === ""))
232
- .map((c) => c
233
- .trim()
234
- // .replace(/\\\|/g, "|")
235
- // .replace(/\\\[/g, "[")
236
- // .replace(/\\\]/g, "]")
237
- );
238
- return `<tr>\n${cells
239
- .map((c, i) => {
240
- const align = aligns[i] ? ` style="text-align:${aligns[i]}"` : "";
241
- return `<td${align}>${renderInline(c, opts)}</td>`;
242
- })
243
- .join("\n")}\n</tr>`;
244
- });
245
- const tbody = `<tbody>\n${bodyRows.join("\n")}\n</tbody>`;
246
- return `<table>\n${thead}\n${tbody}\n</table>`;
247
- }
248
- // ---------------------------------------------------------------------------
249
- // Blockquotes (verschachtelt)
250
- // ---------------------------------------------------------------------------
251
- function parseBlockquote(content, opts) {
252
- // Entferne führendes >
253
- const inner = content
254
- .split("\n")
255
- .map((l) => l.replace(/^>\s?/, ""))
256
- .join("\n");
257
- return `<blockquote>\n${parseBlocks(inner, opts)}\n</blockquote>`;
258
- }
259
- // ---------------------------------------------------------------------------
260
- // Code-Blöcke
261
- // ---------------------------------------------------------------------------
262
- function renderCodeBlock(lang, code) {
263
- const langAttr = lang ? ` class="language-${escapeHtml(lang)}"` : "";
264
- return `<pre><code${langAttr}>${escapeHtml(code)}</code></pre>`;
265
- }
266
- function collectFootnotes(text) {
267
- const notes = {};
268
- const cleaned = text.replace(/^\[(\^[^\]]+)\]:\s+(.+)$/gm, (_, key, val) => {
269
- notes[key] = val;
270
- return "";
271
- });
272
- return { text: cleaned, notes };
273
- }
274
- function renderFootnoteRefs(text, notes, opts) {
275
- return text.replace(/\[(\^[^\]]+)\]/g, (_, key) => {
276
- if (!notes[key])
277
- return _;
278
- const id = key.slice(1);
279
- return `<sup><a href="#fn-${id}" id="fnref-${id}">${id}</a></sup>`;
280
- });
281
- }
282
- function renderFootnoteList(notes, opts) {
283
- const entries = Object.entries(notes);
284
- if (entries.length === 0)
285
- return "";
286
- const items = entries
287
- .map(([key, val]) => {
288
- const id = key.slice(1);
289
- return `<li id="fn-${id}">${renderInline(val, opts)} <a href="#fnref-${id}">↩</a></li>`;
290
- })
291
- .join("\n");
292
- return `<hr>\n<ol class="footnotes">\n${items}\n</ol>`;
293
- }
294
- // ---------------------------------------------------------------------------
295
- // Block-Parser (Kern)
296
- // ---------------------------------------------------------------------------
297
- function parseBlocks(markdown, opts) {
298
- const output = [];
299
- let remaining = markdown;
300
- while (remaining.length > 0) {
301
- let matched = false;
302
- // -----------------------------------------------------------------------
303
- // Leerzeilen überspringen
304
- if (/^\n+/.test(remaining)) {
305
- remaining = remaining.replace(/^\n+/, "");
306
- continue;
307
- }
308
- // -----------------------------------------------------------------------
309
- // Code-Block-Platzhalter (bereits in parse() extrahiert)
310
- {
311
- const m = remaining.match(/^@@CODEBLOCK\d+@@/);
312
- if (m) {
313
- output.push(m[0]); // wird später in parse() ersetzt
314
- remaining = remaining.slice(m[0].length);
315
- matched = true;
316
- }
317
- }
318
- if (matched)
319
- continue;
320
- // -----------------------------------------------------------------------
321
- // Fenced Code Block ```lang\n...\n``` (Fallback)
322
- {
323
- const m = remaining.match(/^(`{3,}|~{3,})([^\n]*)\n([\s\S]*?)\n?\1[ \t]*(?:\n|$)/);
324
- if (m) {
325
- const lang = m[2].trim();
326
- const code = m[3];
327
- output.push(renderCodeBlock(lang, code));
328
- remaining = remaining.slice(m[0].length);
329
- matched = true;
330
- }
331
- }
332
- if (matched)
333
- continue;
334
- // -----------------------------------------------------------------------
335
- // Eingerückter Code Block (4 Leerzeichen oder 1 Tab)
336
- {
337
- const lines = [];
338
- let rest = remaining;
339
- let anyCode = false;
340
- while (true) {
341
- const m = rest.match(/^(?: {4}|\t)(.*)(?:\n|$)/);
342
- if (!m)
343
- break;
344
- lines.push(m[1]);
345
- rest = rest.slice(m[0].length);
346
- anyCode = true;
347
- }
348
- if (anyCode) {
349
- output.push(renderCodeBlock("", lines.join("\n")));
350
- remaining = rest;
351
- matched = true;
352
- }
353
- }
354
- if (matched)
355
- continue;
356
- // -----------------------------------------------------------------------
357
- // Blockquote
358
- {
359
- const lines = [];
360
- let rest = remaining;
361
- while (true) {
362
- const m = rest.match(/^>(.*)(?:\n|$)/);
363
- if (!m)
364
- break;
365
- lines.push(">" + m[1]);
366
- rest = rest.slice(m[0].length);
367
- }
368
- if (lines.length > 0) {
369
- output.push(parseBlockquote(lines.join("\n"), opts));
370
- remaining = rest;
371
- matched = true;
372
- }
373
- }
374
- if (matched)
375
- continue;
376
- // -----------------------------------------------------------------------
377
- // Überschriften # bis ######
378
- {
379
- const m = remaining.match(/^(#{1,6})\s+(.+?)(?:\s+#+)?\s*(?:\n|$)/);
380
- if (m) {
381
- const level = m[1].length;
382
- const text = m[2].trim();
383
- const id = text.toLowerCase().replace(/[^\w\s-]/g, "").replace(/\s+/g, "-");
384
- output.push(`<h${level} id="${id}">${renderInline(text, opts)}</h${level}>`);
385
- remaining = remaining.slice(m[0].length);
386
- matched = true;
387
- }
388
- }
389
- if (matched)
390
- continue;
391
- // -----------------------------------------------------------------------
392
- // Setext-Überschriften
393
- {
394
- const m = remaining.match(/^(.+)\n(=+|-+)\s*(?:\n|$)/);
395
- if (m) {
396
- const level = m[2][0] === "=" ? 1 : 2;
397
- const text = m[1].trim();
398
- const id = text.toLowerCase().replace(/[^\w\s-]/g, "").replace(/\s+/g, "-");
399
- output.push(`<h${level} id="${id}">${renderInline(text, opts)}</h${level}>`);
400
- remaining = remaining.slice(m[0].length);
401
- matched = true;
402
- }
403
- }
404
- if (matched)
405
- continue;
406
- // -----------------------------------------------------------------------
407
- // Horizontale Linie --- / *** / ___
408
- {
409
- const m = remaining.match(/^(?:[-*_] *){3,}\s*(?:\n|$)/);
410
- if (m) {
411
- output.push("<hr>");
412
- remaining = remaining.slice(m[0].length);
413
- matched = true;
414
- }
415
- }
416
- if (matched)
417
- continue;
418
- // -----------------------------------------------------------------------
419
- // Tabelle (enthält | in der ersten Zeile und --- in der zweiten)
420
- {
421
- const m = remaining.match(/^(\|?.+\|.+\n\|?[-| :]+\|[-| :]+\n(?:\|?.+\|.+\n?)*)/);
422
- if (m) {
423
- output.push(parseTable(m[1], opts));
424
- remaining = remaining.slice(m[0].length);
425
- matched = true;
426
- }
427
- }
428
- if (matched)
429
- continue;
430
- // -----------------------------------------------------------------------
431
- // Ungeordnete Liste
432
- {
433
- const lines = [];
434
- let rest = remaining;
435
- while (true) {
436
- const m = rest.match(/^( *[-*+] .*)(?:\n|$)/);
437
- if (!m) {
438
- // Eingerückte Fortsetzungszeilen
439
- const cont = rest.match(/^( {2,}.+)(?:\n|$)/);
440
- if (cont && lines.length > 0) {
441
- lines.push(cont[1]);
442
- rest = rest.slice(cont[0].length);
443
- continue;
444
- }
445
- break;
446
- }
447
- lines.push(m[1]);
448
- rest = rest.slice(m[0].length);
449
- }
450
- if (lines.length > 0) {
451
- const items = parseListItems(lines, 0);
452
- output.push(renderList(items, false, opts));
453
- remaining = rest;
454
- matched = true;
455
- }
456
- }
457
- if (matched)
458
- continue;
459
- // -----------------------------------------------------------------------
460
- // Geordnete Liste
461
- {
462
- const lines = [];
463
- let rest = remaining;
464
- let startNum = 1;
465
- while (true) {
466
- const m = rest.match(/^( *\d+\. .*)(?:\n|$)/);
467
- if (!m) {
468
- const cont = rest.match(/^( {3,}.+)(?:\n|$)/);
469
- if (cont && lines.length > 0) {
470
- lines.push(cont[1]);
471
- rest = rest.slice(cont[0].length);
472
- continue;
473
- }
474
- break;
475
- }
476
- if (lines.length === 0) {
477
- const sn = m[1].match(/^(\d+)\./);
478
- if (sn)
479
- startNum = parseInt(sn[1], 10);
480
- }
481
- lines.push(m[1]);
482
- rest = rest.slice(m[0].length);
483
- }
484
- if (lines.length > 0) {
485
- const items = parseListItems(lines, 0);
486
- const tag = `ol${startNum !== 1 ? ` start="${startNum}"` : ""}`;
487
- output.push(`<${tag}>\n${renderListItems(items, true, opts)}\n</ol>`);
488
- remaining = rest;
489
- matched = true;
490
- }
491
- }
492
- if (matched)
493
- continue;
494
- // -----------------------------------------------------------------------
495
- // Rohes HTML-Block (falls !sanitize)
496
- if (!opts.sanitize) {
497
- const m = remaining.match(/^(<(?:div|section|article|aside|header|footer|nav|main|p|blockquote|pre|table|ul|ol|dl|form|figure|details|summary)[^>]*>[\s\S]*?<\/\w+>)\s*(?:\n|$)/i);
498
- if (m) {
499
- output.push(m[1]);
500
- remaining = remaining.slice(m[0].length);
501
- matched = true;
502
- }
503
- }
504
- if (matched)
505
- continue;
506
- // -----------------------------------------------------------------------
507
- // Absatz (alles bis zur nächsten Leerzeile)
508
- {
509
- const m = remaining.match(/^([\s\S]+?)(?:\n\n|$)/);
510
- if (m) {
511
- const text = m[1].trim();
512
- if (text) {
513
- output.push(`<p>${renderInline(text, opts)}</p>`);
514
- }
515
- remaining = remaining.slice(m[0].length);
516
- matched = true;
517
- }
518
- }
519
- if (matched)
520
- continue;
521
- // Fallback: Zeichen konsumieren
522
- remaining = remaining.slice(1);
523
- }
524
- return output.join("\n");
525
- }
526
- // ---------------------------------------------------------------------------
527
- // Standard-CSS
528
- // ---------------------------------------------------------------------------
529
- const defaultCss = `
530
- :root {
531
- --md-font: system-ui, sans-serif;
532
- --md-mono: "Fira Code", "Cascadia Code", Consolas, monospace;
533
- --md-max-width: 800px;
534
- --md-line-height: 1.7;
535
- --md-color: #1a1a2e;
536
- --md-bg: #ffffff;
537
- --md-code-bg: #f4f4f8;
538
- --md-border: #d1d5db;
539
- --md-accent: #3b5bdb;
540
- --md-blockquote: #6b7280;
541
- }
542
- *, *::before, *::after { box-sizing: border-box; }
543
- body { margin: 0; background: var(--md-bg); color: var(--md-color); }
544
- .md-body {
545
- font-family: var(--md-font);
546
- line-height: var(--md-line-height);
547
- max-width: var(--md-max-width);
548
- margin: 2rem auto;
549
- padding: 0 1.5rem;
550
- }
551
- h1,h2,h3,h4,h5,h6 {
552
- margin: 1.6em 0 0.4em;
553
- line-height: 1.25;
554
- font-weight: 700;
555
- }
556
- h1 { font-size: 2rem; border-bottom: 2px solid var(--md-border); padding-bottom: 0.3em; }
557
- h2 { font-size: 1.5rem; border-bottom: 1px solid var(--md-border); padding-bottom: 0.2em; }
558
- p { margin: 0.8em 0; }
559
- a { color: var(--md-accent); }
560
- code {
561
- font-family: var(--md-mono);
562
- font-size: 0.875em;
563
- background: var(--md-code-bg);
564
- padding: 0.15em 0.35em;
565
- border-radius: 4px;
566
- }
567
- pre { background: var(--md-code-bg); border-radius: 6px; padding: 1em; overflow-x: auto; }
568
- pre code { background: none; padding: 0; font-size: 0.9em; }
569
- blockquote {
570
- margin: 1em 0;
571
- padding: 0.5em 1em;
572
- border-left: 4px solid var(--md-accent);
573
- color: var(--md-blockquote);
574
- }
575
- table { border-collapse: collapse; width: 100%; margin: 1em 0; }
576
- th, td { border: 1px solid var(--md-border); padding: 0.5em 0.8em; }
577
- th { background: var(--md-code-bg); font-weight: 600; }
578
- tr:nth-child(even) td { background: #fafafa; }
579
- ul, ol { padding-left: 1.5em; margin: 0.8em 0; }
580
- li { margin: 0.25em 0; }
581
- hr { border: none; border-top: 2px solid var(--md-border); margin: 2em 0; }
582
- img { max-width: 100%; height: auto; border-radius: 4px; }
583
- mark { background: #fef08a; padding: 0.1em 0.2em; border-radius: 2px; }
584
- input[type="checkbox"] { margin-right: 0.4em; }
585
- .footnotes { font-size: 0.875em; color: var(--md-blockquote); }
586
- `;
587
- // ---------------------------------------------------------------------------
588
- // Öffentliche API
589
- // ---------------------------------------------------------------------------
590
- /**
591
- * Konvertiert Markdown-Text in HTML.
592
- *
593
- * @param markdown Eingabe-Markdown
594
- * @param options Optionale Parser-Einstellungen
595
- * @returns Gerendertes HTML
596
- *
597
- * @example
598
- * ```ts
599
- * import { parse } from "./markdown-parser";
600
- * const html = parse("# Hallo Welt\n\nDas ist **Markdown**.");
601
- * ```
602
- */
603
- export function parse(markdown, options = {}) {
604
- const opts = {
605
- externalLinks: true,
606
- breaks: false,
607
- smartypants: false,
608
- sanitize: false,
609
- ...options,
610
- };
611
- // Zeilenenden normalisieren
612
- let text = markdown.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
613
- // -------------------------------------------------------------------------
614
- // Fenced Code Blocks VOR allem anderen extrahieren und als Platzhalter
615
- // sichern – so kann kein anderer Parser (Inline-Code, Absatz, …) den
616
- // Inhalt anfassen.
617
- // -------------------------------------------------------------------------
618
- const codeBlockPlaceholders = [];
619
- text = text.replace(/^(`{3,}|~{3,})([^\n]*)\n([\s\S]*?)\n?\1[ \t]*(?:\n|$)/gm, (_, fence, lang, code) => {
620
- const idx = codeBlockPlaceholders.push(renderCodeBlock(lang.trim(), code)) - 1;
621
- return `@@CODEBLOCK${idx}@@\n`;
622
- });
623
- // Fußnoten-Definitionen einsammeln
624
- const { text: cleaned, notes } = collectFootnotes(text);
625
- text = cleaned;
626
- // Fußnoten-Referenzen im Text ersetzen
627
- text = renderFootnoteRefs(text, notes, opts);
628
- // Block-Parsing
629
- let html = parseBlocks(text, opts);
630
- // Platzhalter durch gerenderte Code-Blöcke ersetzen
631
- html = html.replace(/@@CODEBLOCK(\d+)@@/g, (_, i) => codeBlockPlaceholders[+i]);
632
- // Fußnoten-Liste anhängen
633
- html += renderFootnoteList(notes, opts);
634
- return html;
635
- }
636
- /**
637
- * Gibt ein vollständiges HTML-Dokument zurück (optional mit eigenem CSS).
638
- */
639
- export function parseToDocument(markdown, options = {}) {
640
- const { title = "Dokument", header = "", css = defaultCss, ...parseOpts } = options;
641
- const body = parse(markdown, parseOpts);
642
- return `<!DOCTYPE html>
643
- <html lang="de">
644
- <head>
645
- <meta charset="UTF-8">
646
- <meta name="viewport" content="width=device-width, initial-scale=1">
647
- <title>${escapeHtml(title)}</title>
648
- <style>${css}</style>
649
- ${header}
650
- </head>
651
- <body>
652
- <article class="md-body">
653
- ${body}
654
- </article>
655
- </body>
656
- </html>`;
657
- }
658
- // Function to export the CSS
659
- export function exportcss() {
660
- return defaultCss;
661
- }
662
- // ---------------------------------------------------------------------------
663
- // CLI (wird ausgeführt wenn die Datei direkt aufgerufen wird)
664
- // ---------------------------------------------------------------------------
665
- // if (typeof process !== "undefined" && process.argv[1]?.endsWith("markdown-parser.ts")) {
666
- // const args = process.argv.slice(2);
667
- // if (args.length === 0) {
668
- // // Demo
669
- // const demo = `
670
- // # Markdown Parser Demo
671
- // Ein vollständiger **Markdown → HTML** Parser in *TypeScript*.
672
- // ## Features
673
- // - Überschriften (H1-H6)
674
- // - **Fett**, *Kursiv*, ~~Durchgestrichen~~, \`Inline-Code\`
675
- // - ==Markiert==, ^Hochgestellt^, ~Tiefgestellt~
676
- // - [Links](https://example.com "Beispiel") und ![Bilder](https://via.placeholder.com/100 "Platzhalter")
677
- // - Aufgabenlisten:
678
- // - [x] Inline-Parser
679
- // - [x] Block-Parser
680
- // - [ ] CLI-Tool
681
- // ## Code
682
- // \`\`\`typescript
683
- // const html = parse("# Hallo");
684
- // console.log(html);
685
- // \`\`\`
686
- // ## Tabelle
687
- // | Spalte | Typ | Pflicht |
688
- // |:-------|:------:|--------:|
689
- // | text | string | ja |
690
- // | id | number | nein |
691
- // > Blockquotes werden auch unterstützt.
692
- // > > Sogar verschachtelt!
693
- // ---
694
- // Fußnoten[^1] funktionieren ebenfalls.
695
- // [^1]: Das ist eine Fußnote.
696
- // `.trim();
697
- // console.log(parseToDocument(demo, { title: "Demo", smartypants: true }));
698
- // }
699
- // }