samengine 1.9.1 → 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.
- package/LICENSE +201 -0
- package/README.md +168 -0
- package/dist/config/buildconfig.d.ts +146 -0
- package/dist/config/buildconfig.js +115 -0
- package/dist/config/index.d.ts +9 -0
- package/dist/config/index.js +1 -0
- package/dist/core.d.ts +17 -0
- package/dist/core.js +24 -0
- package/dist/html.d.ts +29 -0
- package/dist/html.js +20 -0
- package/dist/input.d.ts +51 -0
- package/dist/input.js +44 -3
- package/dist/keys.d.ts +6 -0
- package/dist/keys.js +6 -2
- package/dist/logger.d.ts +8 -0
- package/dist/logger.js +8 -1
- package/dist/nonbrowser/getversion.d.ts +13 -0
- package/dist/nonbrowser/getversion.js +35 -0
- package/dist/nonbrowser/ghresolver.d.ts +1 -0
- package/dist/nonbrowser/ghresolver.js +7 -0
- package/dist/nonbrowser/index.d.ts +9 -0
- package/dist/nonbrowser/index.js +9 -0
- package/dist/nonbrowser/internal/buildhelper.d.ts +42 -0
- package/dist/nonbrowser/internal/buildhelper.js +144 -0
- package/dist/nonbrowser/internal/cli/argparser.d.ts +20 -0
- package/dist/nonbrowser/internal/cli/argparser.js +36 -0
- package/dist/nonbrowser/internal/cli/main.d.ts +13 -0
- package/dist/nonbrowser/internal/cli/main.js +262 -0
- package/dist/nonbrowser/internal/config.d.ts +9 -0
- package/dist/nonbrowser/internal/config.js +40 -0
- package/dist/nonbrowser/internal/exporthtml.d.ts +37 -0
- package/dist/nonbrowser/internal/exporthtml.js +622 -0
- package/dist/nonbrowser/internal/projcreator/downloadZip.d.ts +4 -0
- package/dist/nonbrowser/internal/projcreator/downloadZip.js +83 -0
- package/dist/nonbrowser/internal/projcreator/main.d.ts +1 -0
- package/dist/nonbrowser/internal/projcreator/main.js +81 -0
- package/dist/nonbrowser/utils.d.ts +8 -0
- package/dist/nonbrowser/utils.js +18 -0
- package/dist/physics/collision.d.ts +33 -0
- package/dist/physics/collision.js +27 -0
- package/dist/physics/physicsEngine.d.ts +18 -0
- package/dist/physics/physicsEngine.js +18 -0
- package/dist/physics/physicsObject.d.ts +20 -0
- package/dist/physics/physicsObject.js +20 -0
- package/dist/renderer.d.ts +78 -0
- package/dist/renderer.js +72 -9
- package/dist/samegui/index.d.ts +29 -0
- package/dist/samegui/index.js +26 -0
- package/dist/save.d.ts +12 -0
- package/dist/save.js +10 -0
- package/dist/sound/audioplayer.d.ts +39 -0
- package/dist/sound/audioplayer.js +39 -5
- package/dist/storage/index.d.ts +40 -2
- package/dist/storage/index.js +34 -3
- package/dist/text/index.d.ts +14 -0
- package/dist/text/index.js +58 -0
- package/dist/texture.d.ts +100 -0
- package/dist/texture.js +75 -41
- package/dist/types/button.d.ts +25 -0
- package/dist/types/button.js +22 -0
- package/dist/types/circle.d.ts +26 -0
- package/dist/types/circle.js +21 -7
- package/dist/types/color.d.ts +17 -0
- package/dist/types/color.js +11 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.js +1 -1
- package/dist/types/rectangle.d.ts +29 -0
- package/dist/types/rectangle.js +23 -7
- package/dist/types/triangle.d.ts +23 -0
- package/dist/types/triangle.js +20 -6
- package/dist/types/vector2d.d.ts +42 -0
- package/dist/types/vector2d.js +39 -11
- package/dist/types/vector3d.d.ts +38 -0
- package/dist/types/vector3d.js +35 -11
- package/dist/utils/index.d.ts +11 -4
- package/dist/utils/index.js +11 -4
- package/dist/utils/logger/index.d.ts +24 -0
- package/dist/utils/logger/index.js +44 -0
- package/dist/utils/math.d.ts +18 -0
- package/dist/utils/math.js +18 -4
- package/package.json +36 -11
- package/dist/utils/csv/index.d.ts +0 -3
- package/dist/utils/csv/index.js +0 -2
- package/dist/utils/csv/parser.d.ts +0 -25
- package/dist/utils/csv/parser.js +0 -212
- package/dist/utils/csv/stringifier.d.ts +0 -30
- package/dist/utils/csv/stringifier.js +0 -130
- package/dist/utils/csv/types.d.ts +0 -63
- package/dist/utils/csv/types.js +0 -1
- package/dist/utils/jsonc-parser.d.ts +0 -4
- package/dist/utils/jsonc-parser.js +0 -166
- package/dist/utils/markdown.d.ts +0 -41
- package/dist/utils/markdown.js +0 -699
package/dist/utils/markdown.js
DELETED
|
@@ -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, "<")
|
|
9
|
-
.replace(/>/g, ">")
|
|
10
|
-
.replace(/"/g, """)
|
|
11
|
-
.replace(/'/g, "'");
|
|
12
|
-
}
|
|
13
|
-
function unescapeHtml(text) {
|
|
14
|
-
return text
|
|
15
|
-
.replace(/&/g, "&")
|
|
16
|
-
.replace(/</g, "<")
|
|
17
|
-
.replace(/>/g, ">")
|
|
18
|
-
.replace(/"/g, '"')
|
|
19
|
-
.replace(/'/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 
|
|
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 
|
|
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
|
-
// }
|