samengine 1.6.4

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