samengine 1.8.1 → 1.9.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.
- package/README.md +41 -18
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -2
- package/dist/renderer.d.ts +7 -2
- package/dist/renderer.js +16 -0
- package/dist/samegui/index.d.ts +20 -0
- package/dist/samegui/index.js +111 -0
- package/dist/save.d.ts +18 -0
- package/dist/save.js +15 -0
- package/dist/storage/index.d.ts +19 -0
- package/dist/storage/index.js +58 -0
- package/dist/types/rectangle.js +1 -0
- package/dist/utils/csv/index.d.ts +3 -0
- package/dist/utils/csv/index.js +2 -0
- package/dist/utils/csv/parser.d.ts +25 -0
- package/dist/utils/csv/parser.js +212 -0
- package/dist/utils/csv/stringifier.d.ts +30 -0
- package/dist/utils/csv/stringifier.js +130 -0
- package/dist/utils/csv/types.d.ts +63 -0
- package/dist/utils/csv/types.js +1 -0
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.js +17 -0
- package/dist/utils/markdown.js +36 -27
- package/package.json +10 -5
- package/dist/build/index.d.ts +0 -1
- package/dist/build/index.js +0 -4
package/README.md
CHANGED
|
@@ -4,6 +4,41 @@ A lightweight, TypeScript-first web game engine framework for building
|
|
|
4
4
|
2D games *and maybe 3D Games in the Future*.
|
|
5
5
|
|
|
6
6
|
|
|
7
|
+
<!--$$MD_INDEX_START$$-->
|
|
8
|
+
<!--
|
|
9
|
+
Index by Automatic MD Index
|
|
10
|
+
a simple Tool to Index your Markdown files like this
|
|
11
|
+
|
|
12
|
+
More Infos:
|
|
13
|
+
https://github.com/ShadowDara/automatic-md-index
|
|
14
|
+
|
|
15
|
+
DO NOT REMOVE THIS CREDIT !!!
|
|
16
|
+
|
|
17
|
+
Last Update Time of the Index:
|
|
18
|
+
-->
|
|
19
|
+
|
|
20
|
+
## Index
|
|
21
|
+
- [Features](#features)
|
|
22
|
+
- [Info](#info)
|
|
23
|
+
- [Quick Start](#quick-start)
|
|
24
|
+
- [Basic Game Loop](#basic-game-loop)
|
|
25
|
+
- [Development & Building](#development-building)
|
|
26
|
+
- [Using Bun (local development)](#using-bun-local-development)
|
|
27
|
+
- [Config](#config)
|
|
28
|
+
- [API Reference](#api-reference)
|
|
29
|
+
- [Core Engine](#core-engine)
|
|
30
|
+
- [Rendering](#rendering)
|
|
31
|
+
- [Input System](#input-system)
|
|
32
|
+
- [Types](#types)
|
|
33
|
+
- [Utilities](#utilities)
|
|
34
|
+
- [License](#license)
|
|
35
|
+
- [More Addons in the Game Library](#more-addons-in-the-game-library)
|
|
36
|
+
- [More Tools for samengine and Game Making by me lol](#more-tools-for-samengine-and-game-making-by-me-lol)
|
|
37
|
+
- [Commit Tags](#commit-tags)
|
|
38
|
+
<!-- Index by Automatic MD Index -->
|
|
39
|
+
<!--$$MD_INDEX_END$$-->
|
|
40
|
+
|
|
41
|
+
|
|
7
42
|
## Features
|
|
8
43
|
|
|
9
44
|
- 🎯 Simple game loop management
|
|
@@ -15,6 +50,11 @@ A lightweight, TypeScript-first web game engine framework for building
|
|
|
15
50
|
- 💾 Save/Load system
|
|
16
51
|
|
|
17
52
|
|
|
53
|
+
## Info
|
|
54
|
+
|
|
55
|
+
For better Infos read the [Docs](samengine.vercel.app/docs)
|
|
56
|
+
|
|
57
|
+
|
|
18
58
|
## Quick Start
|
|
19
59
|
|
|
20
60
|
```sh
|
|
@@ -70,24 +110,7 @@ npx samengine-build --new (newproject) # Create a new project with a
|
|
|
70
110
|
npx samengine-build --new-empty (new) # Create a new empty project
|
|
71
111
|
```
|
|
72
112
|
|
|
73
|
-
|
|
74
|
-
### Configuration
|
|
75
|
-
|
|
76
|
-
Edit `samengine.config.ts` to configure your game:
|
|
77
|
-
|
|
78
|
-
```typescript
|
|
79
|
-
import { defineConfig } from 'samengine-build';
|
|
80
|
-
|
|
81
|
-
export function defineConfig() {
|
|
82
|
-
return {
|
|
83
|
-
entryname: 'main',
|
|
84
|
-
outdir: 'dist',
|
|
85
|
-
// ... other config
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
or
|
|
113
|
+
## Config
|
|
91
114
|
|
|
92
115
|
```typescript
|
|
93
116
|
// Project File for the Game
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { startEngine } from "./core.js";
|
|
2
|
-
export {
|
|
2
|
+
export type { CharMap, ParallaxLayer } from "./renderer.js";
|
|
3
|
+
export { renderText, renderBitmapText, drawRect, drawRectOutline, drawCircle, drawCircleOutline, drawTriangle, drawTriangleOutline, renderParallaxBackground, renderParallaxLayers, } from "./renderer.js";
|
|
3
4
|
export type { Mouse } from "./input.js";
|
|
4
5
|
export { setupInput, isKeyPressed, isKeyJustPressed, isKeyJustReleased, resetInput, getMouse } from "./input.js";
|
|
5
6
|
export { dlog } from "./logger.js";
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
// Core Engine Exports
|
|
2
2
|
export { startEngine } from "./core.js";
|
|
3
|
-
|
|
4
|
-
export { renderText, renderBitmapText, drawRect, drawRectOutline, drawCircle, drawCircleOutline, drawTriangle, drawTriangleOutline, } from "./renderer.js";
|
|
3
|
+
export { renderText, renderBitmapText, drawRect, drawRectOutline, drawCircle, drawCircleOutline, drawTriangle, drawTriangleOutline, renderParallaxBackground, renderParallaxLayers, } from "./renderer.js";
|
|
5
4
|
export { setupInput, isKeyPressed, isKeyJustPressed, isKeyJustReleased, resetInput, getMouse } from "./input.js";
|
|
6
5
|
// Logging
|
|
7
6
|
export { dlog } from "./logger.js";
|
package/dist/renderer.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { type Rect } from "./types/rectangle.js";
|
|
|
2
2
|
import { type Circle } from "./types/circle.js";
|
|
3
3
|
import { type Triangle } from "./types/triangle.js";
|
|
4
4
|
export declare function renderText(ctx: CanvasRenderingContext2D, text: string, x: number, y: number, color?: string, font?: string): void;
|
|
5
|
-
type CharMap = Record<string, Rect>;
|
|
5
|
+
export type CharMap = Record<string, Rect>;
|
|
6
6
|
export declare function renderBitmapText(ctx: CanvasRenderingContext2D, text: string, x: number, y: number, sprite: HTMLImageElement, charMap: CharMap, scale?: number): void;
|
|
7
7
|
export declare function drawRect(ctx: CanvasRenderingContext2D, rect: Rect, color?: string): void;
|
|
8
8
|
export declare function drawRectOutline(ctx: CanvasRenderingContext2D, rect: Rect, color?: string, lineWidth?: number): void;
|
|
@@ -10,4 +10,9 @@ export declare function drawCircle(ctx: CanvasRenderingContext2D, circle: Circle
|
|
|
10
10
|
export declare function drawCircleOutline(ctx: CanvasRenderingContext2D, circle: Circle, color?: string, lineWidth?: number): void;
|
|
11
11
|
export declare function drawTriangle(ctx: CanvasRenderingContext2D, triangle: Triangle, color?: string): void;
|
|
12
12
|
export declare function drawTriangleOutline(ctx: CanvasRenderingContext2D, triangle: Triangle, color?: string, lineWidth?: number): void;
|
|
13
|
-
export
|
|
13
|
+
export declare function renderParallaxBackground(ctx: CanvasRenderingContext2D, image: HTMLImageElement, cameraX: number, speed?: number, canvasWidth?: number, canvasHeight?: number): void;
|
|
14
|
+
export interface ParallaxLayer {
|
|
15
|
+
image: HTMLImageElement;
|
|
16
|
+
speed: number;
|
|
17
|
+
}
|
|
18
|
+
export declare function renderParallaxLayers(ctx: CanvasRenderingContext2D, layers: ParallaxLayer[], cameraX: number): void;
|
package/dist/renderer.js
CHANGED
|
@@ -79,3 +79,19 @@ export function drawTriangleOutline(ctx, triangle, color = "white", lineWidth =
|
|
|
79
79
|
ctx.closePath();
|
|
80
80
|
ctx.stroke();
|
|
81
81
|
}
|
|
82
|
+
// Function to render a parallax background layer
|
|
83
|
+
export function renderParallaxBackground(ctx, image, cameraX, speed = 0.5, canvasWidth = ctx.canvas.width, canvasHeight = ctx.canvas.height) {
|
|
84
|
+
if (!image.complete)
|
|
85
|
+
return;
|
|
86
|
+
const offsetX = -(cameraX * speed) % image.width;
|
|
87
|
+
// Draw enough copies to fill the screen
|
|
88
|
+
for (let x = offsetX - image.width; x < canvasWidth; x += image.width) {
|
|
89
|
+
ctx.drawImage(image, x, 0, image.width, canvasHeight);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// Render multiple paralax Layers
|
|
93
|
+
export function renderParallaxLayers(ctx, layers, cameraX) {
|
|
94
|
+
for (const layer of layers) {
|
|
95
|
+
renderParallaxBackground(ctx, layer.image, cameraX, layer.speed);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type UIElementType = "button" | "text" | "checkbox";
|
|
2
|
+
export type UIElement = {
|
|
3
|
+
el: HTMLElement;
|
|
4
|
+
type: UIElementType;
|
|
5
|
+
};
|
|
6
|
+
export declare class HtmlUI {
|
|
7
|
+
private root;
|
|
8
|
+
private panel;
|
|
9
|
+
private elements;
|
|
10
|
+
private clicked;
|
|
11
|
+
private frameIds;
|
|
12
|
+
private valueMap;
|
|
13
|
+
constructor();
|
|
14
|
+
begin(): void;
|
|
15
|
+
end(): void;
|
|
16
|
+
private getButton;
|
|
17
|
+
button(label: string, idOverride?: string): boolean;
|
|
18
|
+
text(label: string, idOverride?: string): void;
|
|
19
|
+
checkbox(label: string, defaultValue: boolean, idOverride?: string): boolean;
|
|
20
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// HTML Overlay
|
|
2
|
+
import { hash } from "../utils/index.js";
|
|
3
|
+
export class HtmlUI {
|
|
4
|
+
constructor() {
|
|
5
|
+
this.elements = new Map();
|
|
6
|
+
this.clicked = new Map();
|
|
7
|
+
this.frameIds = [];
|
|
8
|
+
this.valueMap = new Map();
|
|
9
|
+
this.root = document.createElement("div");
|
|
10
|
+
this.root.style.position = "absolute";
|
|
11
|
+
this.root.style.inset = "0";
|
|
12
|
+
this.root.style.pointerEvents = "none";
|
|
13
|
+
document.body.appendChild(this.root);
|
|
14
|
+
this.panel = document.createElement("div");
|
|
15
|
+
this.panel.style.position = "absolute";
|
|
16
|
+
this.panel.style.left = "20px";
|
|
17
|
+
this.panel.style.top = "20px";
|
|
18
|
+
this.panel.style.padding = "10px";
|
|
19
|
+
this.panel.style.background = "rgba(30,30,30,0.9)";
|
|
20
|
+
this.panel.style.color = "white";
|
|
21
|
+
this.panel.style.pointerEvents = "auto";
|
|
22
|
+
this.panel.style.borderRadius = "6px";
|
|
23
|
+
this.root.appendChild(this.panel);
|
|
24
|
+
}
|
|
25
|
+
begin() {
|
|
26
|
+
this.frameIds = [];
|
|
27
|
+
}
|
|
28
|
+
end() {
|
|
29
|
+
// reset clicks AFTER frame
|
|
30
|
+
for (const id of this.frameIds) {
|
|
31
|
+
this.clicked.set(id, false);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
getButton(id) {
|
|
35
|
+
let e = this.elements.get(id);
|
|
36
|
+
if (!e) {
|
|
37
|
+
const btn = document.createElement("button");
|
|
38
|
+
btn.style.display = "block";
|
|
39
|
+
btn.style.marginBottom = "6px";
|
|
40
|
+
btn.style.width = "100%";
|
|
41
|
+
btn.style.padding = "6px";
|
|
42
|
+
btn.style.background = "#444";
|
|
43
|
+
btn.style.color = "white";
|
|
44
|
+
btn.style.border = "none";
|
|
45
|
+
btn.style.cursor = "pointer";
|
|
46
|
+
this.panel.appendChild(btn);
|
|
47
|
+
e = { el: btn, type: "button" };
|
|
48
|
+
this.elements.set(id, e);
|
|
49
|
+
}
|
|
50
|
+
return e.el;
|
|
51
|
+
}
|
|
52
|
+
button(label, idOverride) {
|
|
53
|
+
const id = hash(idOverride ?? label);
|
|
54
|
+
this.frameIds.push(id);
|
|
55
|
+
const btn = this.getButton(id);
|
|
56
|
+
btn.textContent = label;
|
|
57
|
+
// wichtig: nur EIN handler (kein stacking!)
|
|
58
|
+
btn.onclick = () => {
|
|
59
|
+
this.clicked.set(id, true);
|
|
60
|
+
};
|
|
61
|
+
return this.clicked.get(id) === true;
|
|
62
|
+
}
|
|
63
|
+
text(label, idOverride) {
|
|
64
|
+
const id = hash(idOverride ?? label);
|
|
65
|
+
this.frameIds.push(id);
|
|
66
|
+
let e = this.elements.get(id);
|
|
67
|
+
if (!e) {
|
|
68
|
+
const div = document.createElement("div");
|
|
69
|
+
div.style.marginBottom = "6px";
|
|
70
|
+
this.panel.appendChild(div);
|
|
71
|
+
e = { el: div, type: "text" };
|
|
72
|
+
this.elements.set(id, e);
|
|
73
|
+
}
|
|
74
|
+
e.el.textContent = label;
|
|
75
|
+
}
|
|
76
|
+
checkbox(label, defaultValue, idOverride) {
|
|
77
|
+
const id = hash(idOverride ?? label);
|
|
78
|
+
this.frameIds.push(id);
|
|
79
|
+
// initial state only once
|
|
80
|
+
if (!this.valueMap.has(id)) {
|
|
81
|
+
this.valueMap.set(id, defaultValue);
|
|
82
|
+
}
|
|
83
|
+
const current = this.valueMap.get(id);
|
|
84
|
+
let e = this.elements.get(id);
|
|
85
|
+
if (!e) {
|
|
86
|
+
const wrapper = document.createElement("label");
|
|
87
|
+
wrapper.style.display = "flex";
|
|
88
|
+
wrapper.style.alignItems = "center";
|
|
89
|
+
wrapper.style.gap = "8px";
|
|
90
|
+
wrapper.style.marginBottom = "6px";
|
|
91
|
+
wrapper.style.cursor = "pointer";
|
|
92
|
+
const input = document.createElement("input");
|
|
93
|
+
input.type = "checkbox";
|
|
94
|
+
const text = document.createElement("span");
|
|
95
|
+
wrapper.appendChild(input);
|
|
96
|
+
wrapper.appendChild(text);
|
|
97
|
+
this.panel.appendChild(wrapper);
|
|
98
|
+
e = { el: wrapper, type: "checkbox" };
|
|
99
|
+
this.elements.set(id, e);
|
|
100
|
+
}
|
|
101
|
+
const wrapper = e.el;
|
|
102
|
+
const input = wrapper.querySelector("input");
|
|
103
|
+
const text = wrapper.querySelector("span");
|
|
104
|
+
text.textContent = label;
|
|
105
|
+
input.checked = current;
|
|
106
|
+
input.onchange = () => {
|
|
107
|
+
this.valueMap.set(id, input.checked);
|
|
108
|
+
};
|
|
109
|
+
return current;
|
|
110
|
+
}
|
|
111
|
+
}
|
package/dist/save.d.ts
CHANGED
|
@@ -1,6 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @deprecated Use samengine/storage instead
|
|
3
|
+
*/
|
|
1
4
|
export type SaveData = Record<string, any>;
|
|
5
|
+
/**
|
|
6
|
+
* @deprecated Use samengine/storage instead
|
|
7
|
+
*/
|
|
2
8
|
export declare let SAVE_KEY: string;
|
|
9
|
+
/**
|
|
10
|
+
* @deprecated Use samengine/storage instead
|
|
11
|
+
*/
|
|
3
12
|
export declare function saveGame(data: SaveData): void;
|
|
13
|
+
/**
|
|
14
|
+
* @deprecated Use samengine/storage instead
|
|
15
|
+
*/
|
|
4
16
|
export declare function loadGame(): SaveData | null;
|
|
17
|
+
/**
|
|
18
|
+
* @deprecated Use samengine/storage instead
|
|
19
|
+
*/
|
|
5
20
|
export declare function clearSave(): void;
|
|
21
|
+
/**
|
|
22
|
+
* @deprecated Use samengine/storage instead
|
|
23
|
+
*/
|
|
6
24
|
export declare function exportSave(): void;
|
package/dist/save.js
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
// Please rename the SaveKey
|
|
2
|
+
/**
|
|
3
|
+
* @deprecated Use samengine/storage instead
|
|
4
|
+
*/
|
|
2
5
|
export let SAVE_KEY = "my_game_save";
|
|
6
|
+
/**
|
|
7
|
+
* @deprecated Use samengine/storage instead
|
|
8
|
+
*/
|
|
3
9
|
export function saveGame(data) {
|
|
4
10
|
try {
|
|
5
11
|
const json = JSON.stringify(data);
|
|
@@ -10,6 +16,9 @@ export function saveGame(data) {
|
|
|
10
16
|
console.error("Save failed:", err);
|
|
11
17
|
}
|
|
12
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* @deprecated Use samengine/storage instead
|
|
21
|
+
*/
|
|
13
22
|
export function loadGame() {
|
|
14
23
|
try {
|
|
15
24
|
const json = localStorage.getItem(SAVE_KEY);
|
|
@@ -22,10 +31,16 @@ export function loadGame() {
|
|
|
22
31
|
return null;
|
|
23
32
|
}
|
|
24
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* @deprecated Use samengine/storage instead
|
|
36
|
+
*/
|
|
25
37
|
export function clearSave() {
|
|
26
38
|
localStorage.removeItem(SAVE_KEY);
|
|
27
39
|
console.log("Save cleared!");
|
|
28
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* @deprecated Use samengine/storage instead
|
|
43
|
+
*/
|
|
29
44
|
export function exportSave() {
|
|
30
45
|
try {
|
|
31
46
|
const json = localStorage.getItem(SAVE_KEY);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export type StoredValue<T> = {
|
|
2
|
+
value: T;
|
|
3
|
+
expiresAt?: number;
|
|
4
|
+
};
|
|
5
|
+
export declare class StorageLib {
|
|
6
|
+
static set<T>(key: string, value: T): void;
|
|
7
|
+
static get<T>(key: string): T | null;
|
|
8
|
+
static remove(key: string): void;
|
|
9
|
+
static clear(): void;
|
|
10
|
+
static has(key: string): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Gesamten Storage als JSON exportieren
|
|
13
|
+
*/
|
|
14
|
+
static exportToJson(pretty?: boolean): string;
|
|
15
|
+
/**
|
|
16
|
+
* JSON in den Storage importieren
|
|
17
|
+
*/
|
|
18
|
+
static importFromJson(json: string, overwrite?: boolean): void;
|
|
19
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// Store Data in the Browser
|
|
2
|
+
export class StorageLib {
|
|
3
|
+
static set(key, value) {
|
|
4
|
+
localStorage.setItem(key, JSON.stringify(value));
|
|
5
|
+
}
|
|
6
|
+
static get(key) {
|
|
7
|
+
const item = localStorage.getItem(key);
|
|
8
|
+
if (!item) {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
try {
|
|
12
|
+
return JSON.parse(item);
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
static remove(key) {
|
|
19
|
+
localStorage.removeItem(key);
|
|
20
|
+
}
|
|
21
|
+
static clear() {
|
|
22
|
+
localStorage.clear();
|
|
23
|
+
}
|
|
24
|
+
static has(key) {
|
|
25
|
+
return localStorage.getItem(key) !== null;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Gesamten Storage als JSON exportieren
|
|
29
|
+
*/
|
|
30
|
+
static exportToJson(pretty = true) {
|
|
31
|
+
const data = {};
|
|
32
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
33
|
+
const key = localStorage.key(i);
|
|
34
|
+
if (!key)
|
|
35
|
+
continue;
|
|
36
|
+
const value = localStorage.getItem(key);
|
|
37
|
+
try {
|
|
38
|
+
data[key] = value ? JSON.parse(value) : null;
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
data[key] = value;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return JSON.stringify(data, null, pretty ? 2 : 0);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* JSON in den Storage importieren
|
|
48
|
+
*/
|
|
49
|
+
static importFromJson(json, overwrite = true) {
|
|
50
|
+
const data = JSON.parse(json);
|
|
51
|
+
for (const [key, value] of Object.entries(data)) {
|
|
52
|
+
if (!overwrite && localStorage.getItem(key) !== null) {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
localStorage.setItem(key, JSON.stringify(value));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
package/dist/types/rectangle.js
CHANGED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { CSVParserOptions, ParseResult } from "./types";
|
|
2
|
+
/**
|
|
3
|
+
* RFC 4180-konformer CSV-Parser mit erweitertem Escaping.
|
|
4
|
+
*
|
|
5
|
+
* Unterstützt:
|
|
6
|
+
* - Gequotete Felder mit eingebetteten Zeilenumbrüchen, Trennzeichen und Anführungszeichen
|
|
7
|
+
* - Doppelt-Anführungszeichen-Escaping: "" → "
|
|
8
|
+
* - Optionales Backslash-Escaping: \" → "
|
|
9
|
+
* - Konfigurierbare Trennzeichen, Anführungszeichen, Kommentar-Zeilen
|
|
10
|
+
* - Warnung bei ungleicher Feldanzahl
|
|
11
|
+
*/
|
|
12
|
+
export declare class CSVParser {
|
|
13
|
+
private readonly opts;
|
|
14
|
+
constructor(options?: CSVParserOptions);
|
|
15
|
+
/**
|
|
16
|
+
* Parst einen CSV-String und gibt strukturierte Daten zurück.
|
|
17
|
+
*/
|
|
18
|
+
parse<T = Record<string, string>>(text: string): ParseResult<T>;
|
|
19
|
+
/**
|
|
20
|
+
* Parst einen CSV-String und gibt ausschließlich die Records zurück.
|
|
21
|
+
* Kurzform für einfache Anwendungsfälle.
|
|
22
|
+
*/
|
|
23
|
+
parseRecords<T = Record<string, string>>(text: string): T[];
|
|
24
|
+
private tokenize;
|
|
25
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
const DEFAULT_OPTIONS = {
|
|
2
|
+
delimiter: ",",
|
|
3
|
+
quoteChar: '"',
|
|
4
|
+
escapeChar: "",
|
|
5
|
+
hasHeader: true,
|
|
6
|
+
trimFields: false,
|
|
7
|
+
skipEmptyLines: true,
|
|
8
|
+
commentChar: "",
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* RFC 4180-konformer CSV-Parser mit erweitertem Escaping.
|
|
12
|
+
*
|
|
13
|
+
* Unterstützt:
|
|
14
|
+
* - Gequotete Felder mit eingebetteten Zeilenumbrüchen, Trennzeichen und Anführungszeichen
|
|
15
|
+
* - Doppelt-Anführungszeichen-Escaping: "" → "
|
|
16
|
+
* - Optionales Backslash-Escaping: \" → "
|
|
17
|
+
* - Konfigurierbare Trennzeichen, Anführungszeichen, Kommentar-Zeilen
|
|
18
|
+
* - Warnung bei ungleicher Feldanzahl
|
|
19
|
+
*/
|
|
20
|
+
export class CSVParser {
|
|
21
|
+
constructor(options = {}) {
|
|
22
|
+
this.opts = { ...DEFAULT_OPTIONS, ...options };
|
|
23
|
+
if (this.opts.delimiter.length !== 1) {
|
|
24
|
+
throw new Error("delimiter muss genau ein Zeichen lang sein.");
|
|
25
|
+
}
|
|
26
|
+
if (this.opts.quoteChar.length !== 1) {
|
|
27
|
+
throw new Error("quoteChar muss genau ein Zeichen lang sein.");
|
|
28
|
+
}
|
|
29
|
+
if (this.opts.escapeChar && this.opts.escapeChar.length !== 1) {
|
|
30
|
+
throw new Error("escapeChar muss genau ein Zeichen lang sein.");
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Parst einen CSV-String und gibt strukturierte Daten zurück.
|
|
35
|
+
*/
|
|
36
|
+
parse(text) {
|
|
37
|
+
const rawRows = this.tokenize(text);
|
|
38
|
+
const warnings = [];
|
|
39
|
+
if (rawRows.length === 0) {
|
|
40
|
+
return { records: [], headers: [], rawRows: [], warnings };
|
|
41
|
+
}
|
|
42
|
+
let headers = [];
|
|
43
|
+
let dataRows;
|
|
44
|
+
if (this.opts.hasHeader) {
|
|
45
|
+
headers = rawRows[0].map((h) => this.opts.trimFields ? h.trim() : h);
|
|
46
|
+
dataRows = rawRows.slice(1);
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
// Numerische Header generieren: "0", "1", "2", ...
|
|
50
|
+
const width = rawRows[0].length;
|
|
51
|
+
headers = Array.from({ length: width }, (_, i) => String(i));
|
|
52
|
+
dataRows = rawRows;
|
|
53
|
+
}
|
|
54
|
+
const records = [];
|
|
55
|
+
for (let i = 0; i < dataRows.length; i++) {
|
|
56
|
+
const row = dataRows[i];
|
|
57
|
+
if (row.length !== headers.length) {
|
|
58
|
+
warnings.push({
|
|
59
|
+
row: i + (this.opts.hasHeader ? 2 : 1),
|
|
60
|
+
message: `Zeile hat ${row.length} Felder, erwartet ${headers.length}.`,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
const record = {};
|
|
64
|
+
for (let j = 0; j < headers.length; j++) {
|
|
65
|
+
let value = row[j] ?? "";
|
|
66
|
+
if (this.opts.trimFields)
|
|
67
|
+
value = value.trim();
|
|
68
|
+
record[headers[j]] = value;
|
|
69
|
+
}
|
|
70
|
+
records.push(record);
|
|
71
|
+
}
|
|
72
|
+
return { records, headers, rawRows, warnings };
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Parst einen CSV-String und gibt ausschließlich die Records zurück.
|
|
76
|
+
* Kurzform für einfache Anwendungsfälle.
|
|
77
|
+
*/
|
|
78
|
+
parseRecords(text) {
|
|
79
|
+
return this.parse(text).records;
|
|
80
|
+
}
|
|
81
|
+
// ─────────────────────────────────────────────
|
|
82
|
+
// Privater Tokenizer (Zustandsmaschine)
|
|
83
|
+
// ─────────────────────────────────────────────
|
|
84
|
+
tokenize(text) {
|
|
85
|
+
const { delimiter, quoteChar, escapeChar, skipEmptyLines, commentChar } = this.opts;
|
|
86
|
+
const rows = [];
|
|
87
|
+
let row = [];
|
|
88
|
+
let field = "";
|
|
89
|
+
let inQuotes = false;
|
|
90
|
+
let i = 0;
|
|
91
|
+
const len = text.length;
|
|
92
|
+
const pushRow = () => {
|
|
93
|
+
const isEmpty = row.length === 1 && row[0] === "" && field === "";
|
|
94
|
+
if (skipEmptyLines && isEmpty)
|
|
95
|
+
return;
|
|
96
|
+
if (commentChar && row.length === 0 && field.startsWith(commentChar))
|
|
97
|
+
return;
|
|
98
|
+
row.push(field);
|
|
99
|
+
rows.push(row);
|
|
100
|
+
row = [];
|
|
101
|
+
field = "";
|
|
102
|
+
};
|
|
103
|
+
while (i < len) {
|
|
104
|
+
const ch = text[i];
|
|
105
|
+
const next = i + 1 < len ? text[i + 1] : "";
|
|
106
|
+
if (inQuotes) {
|
|
107
|
+
// Backslash-Escape innerhalb von Quotes
|
|
108
|
+
if (escapeChar && ch === escapeChar && next === quoteChar) {
|
|
109
|
+
field += quoteChar;
|
|
110
|
+
i += 2;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
// Doppeltes Anführungszeichen → ein literales Anführungszeichen
|
|
114
|
+
if (ch === quoteChar && next === quoteChar) {
|
|
115
|
+
field += quoteChar;
|
|
116
|
+
i += 2;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
// Schließendes Anführungszeichen
|
|
120
|
+
if (ch === quoteChar) {
|
|
121
|
+
inQuotes = false;
|
|
122
|
+
i++;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
field += ch;
|
|
126
|
+
i++;
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
// Außerhalb von Quotes:
|
|
130
|
+
// Kommentarzeile (nur am Zeilenanfang)
|
|
131
|
+
if (commentChar && ch === commentChar && row.length === 0 && field === "") {
|
|
132
|
+
// Rest der Zeile überspringen
|
|
133
|
+
while (i < len && text[i] !== "\n" && text[i] !== "\r")
|
|
134
|
+
i++;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
// Öffnendes Anführungszeichen
|
|
138
|
+
if (ch === quoteChar) {
|
|
139
|
+
inQuotes = true;
|
|
140
|
+
i++;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
// Backslash-Escape außerhalb von Quotes (optionales Feature)
|
|
144
|
+
if (escapeChar && ch === escapeChar) {
|
|
145
|
+
if (next === "n") {
|
|
146
|
+
field += "\n";
|
|
147
|
+
i += 2;
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (next === "r") {
|
|
151
|
+
field += "\r";
|
|
152
|
+
i += 2;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (next === "t") {
|
|
156
|
+
field += "\t";
|
|
157
|
+
i += 2;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
if (next === "0") {
|
|
161
|
+
field += "\0";
|
|
162
|
+
i += 2;
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (next === escapeChar) {
|
|
166
|
+
field += escapeChar;
|
|
167
|
+
i += 2;
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
if (next === delimiter) {
|
|
171
|
+
field += delimiter;
|
|
172
|
+
i += 2;
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
// Unbekannte Escape-Sequenz: Backslash literal übernehmen
|
|
176
|
+
field += ch;
|
|
177
|
+
i++;
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
// Trennzeichen
|
|
181
|
+
if (ch === delimiter) {
|
|
182
|
+
row.push(field);
|
|
183
|
+
field = "";
|
|
184
|
+
i++;
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
// Zeilenumbruch \r\n
|
|
188
|
+
if (ch === "\r" && next === "\n") {
|
|
189
|
+
pushRow();
|
|
190
|
+
i += 2;
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
// Zeilenumbruch \r oder \n
|
|
194
|
+
if (ch === "\n" || ch === "\r") {
|
|
195
|
+
pushRow();
|
|
196
|
+
i++;
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
field += ch;
|
|
200
|
+
i++;
|
|
201
|
+
}
|
|
202
|
+
// Letzte Zeile (kein abschließendes Newline)
|
|
203
|
+
if (row.length > 0 || field !== "") {
|
|
204
|
+
row.push(field);
|
|
205
|
+
const isEmpty = row.length === 1 && row[0] === "";
|
|
206
|
+
if (!(skipEmptyLines && isEmpty)) {
|
|
207
|
+
rows.push(row);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return rows;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { CSVStringifierOptions } from "./types";
|
|
2
|
+
/**
|
|
3
|
+
* Serialisiert JavaScript-Objekte zu einem validen CSV-String.
|
|
4
|
+
*
|
|
5
|
+
* Unterstützt:
|
|
6
|
+
* - RFC 4180 Doppelquote-Escaping: " → ""
|
|
7
|
+
* - Optionales Backslash-Escaping: \ → \\
|
|
8
|
+
* - Optionales Null-Byte-Escaping: \0 → \\0
|
|
9
|
+
* - Automatisches Quoting bei Sonderzeichen
|
|
10
|
+
* - Konfigurierbare Spaltenreihenfolge
|
|
11
|
+
* - Wahlweise immer quoten (alwaysQuote)
|
|
12
|
+
*/
|
|
13
|
+
export declare class CSVStringifier {
|
|
14
|
+
private readonly opts;
|
|
15
|
+
private readonly escapeRules;
|
|
16
|
+
constructor(options?: CSVStringifierOptions);
|
|
17
|
+
/**
|
|
18
|
+
* Serialisiert ein Array von Objekten zu einem CSV-String.
|
|
19
|
+
*/
|
|
20
|
+
stringify(records: Record<string, unknown>[]): string;
|
|
21
|
+
/**
|
|
22
|
+
* Serialisiert ein einzelnes Feld mit korrektem Escaping.
|
|
23
|
+
* Öffentlich, damit einzelne Werte unabhängig escaped werden können.
|
|
24
|
+
*/
|
|
25
|
+
escapeField(raw: string): string;
|
|
26
|
+
/**
|
|
27
|
+
* Streamt Records zeilenweise als Generator (speicherschonend für große Dateien).
|
|
28
|
+
*/
|
|
29
|
+
stream(records: Iterable<Record<string, unknown>>, columns: string[]): Generator<string>;
|
|
30
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
const DEFAULT_ESCAPE_RULES = {
|
|
2
|
+
quote: true,
|
|
3
|
+
newline: true,
|
|
4
|
+
delimiter: true,
|
|
5
|
+
backslash: false,
|
|
6
|
+
nullByte: false,
|
|
7
|
+
};
|
|
8
|
+
const DEFAULT_OPTIONS = {
|
|
9
|
+
delimiter: ",",
|
|
10
|
+
quoteChar: '"',
|
|
11
|
+
lineEnding: "\r\n",
|
|
12
|
+
escapeRules: DEFAULT_ESCAPE_RULES,
|
|
13
|
+
alwaysQuote: false,
|
|
14
|
+
columns: [],
|
|
15
|
+
writeHeader: true,
|
|
16
|
+
nullAsEmpty: true,
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Serialisiert JavaScript-Objekte zu einem validen CSV-String.
|
|
20
|
+
*
|
|
21
|
+
* Unterstützt:
|
|
22
|
+
* - RFC 4180 Doppelquote-Escaping: " → ""
|
|
23
|
+
* - Optionales Backslash-Escaping: \ → \\
|
|
24
|
+
* - Optionales Null-Byte-Escaping: \0 → \\0
|
|
25
|
+
* - Automatisches Quoting bei Sonderzeichen
|
|
26
|
+
* - Konfigurierbare Spaltenreihenfolge
|
|
27
|
+
* - Wahlweise immer quoten (alwaysQuote)
|
|
28
|
+
*/
|
|
29
|
+
export class CSVStringifier {
|
|
30
|
+
constructor(options = {}) {
|
|
31
|
+
this.opts = {
|
|
32
|
+
...DEFAULT_OPTIONS,
|
|
33
|
+
...options,
|
|
34
|
+
escapeRules: {
|
|
35
|
+
...DEFAULT_ESCAPE_RULES,
|
|
36
|
+
...(options.escapeRules ?? {}),
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
this.escapeRules = this.opts.escapeRules;
|
|
40
|
+
if (this.opts.delimiter.length !== 1) {
|
|
41
|
+
throw new Error("delimiter muss genau ein Zeichen lang sein.");
|
|
42
|
+
}
|
|
43
|
+
if (this.opts.quoteChar.length !== 1) {
|
|
44
|
+
throw new Error("quoteChar muss genau ein Zeichen lang sein.");
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Serialisiert ein Array von Objekten zu einem CSV-String.
|
|
49
|
+
*/
|
|
50
|
+
stringify(records) {
|
|
51
|
+
if (records.length === 0)
|
|
52
|
+
return "";
|
|
53
|
+
const columns = this.opts.columns.length > 0
|
|
54
|
+
? this.opts.columns
|
|
55
|
+
: Object.keys(records[0]);
|
|
56
|
+
const lines = [];
|
|
57
|
+
if (this.opts.writeHeader) {
|
|
58
|
+
lines.push(columns.map((h) => this.escapeField(h)).join(this.opts.delimiter));
|
|
59
|
+
}
|
|
60
|
+
for (const record of records) {
|
|
61
|
+
const row = columns.map((col) => {
|
|
62
|
+
const value = record[col];
|
|
63
|
+
if ((value === null || value === undefined) && this.opts.nullAsEmpty) {
|
|
64
|
+
return "";
|
|
65
|
+
}
|
|
66
|
+
return this.escapeField(String(value ?? ""));
|
|
67
|
+
});
|
|
68
|
+
lines.push(row.join(this.opts.delimiter));
|
|
69
|
+
}
|
|
70
|
+
return lines.join(this.opts.lineEnding);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Serialisiert ein einzelnes Feld mit korrektem Escaping.
|
|
74
|
+
* Öffentlich, damit einzelne Werte unabhängig escaped werden können.
|
|
75
|
+
*/
|
|
76
|
+
escapeField(raw) {
|
|
77
|
+
const { delimiter, quoteChar, alwaysQuote } = this.opts;
|
|
78
|
+
const rules = this.escapeRules;
|
|
79
|
+
let value = raw;
|
|
80
|
+
let needsQuoting = alwaysQuote;
|
|
81
|
+
// 1. Backslash escapen (muss vor allen anderen stehen)
|
|
82
|
+
if (rules.backslash && value.includes("\\")) {
|
|
83
|
+
value = value.replace(/\\/g, "\\\\");
|
|
84
|
+
needsQuoting = true;
|
|
85
|
+
}
|
|
86
|
+
// 2. Null-Byte escapen
|
|
87
|
+
if (rules.nullByte && value.includes("\0")) {
|
|
88
|
+
value = value.replace(/\0/g, "\\0");
|
|
89
|
+
needsQuoting = true;
|
|
90
|
+
}
|
|
91
|
+
// 3. Anführungszeichen escapen (RFC 4180: "" Verdopplung)
|
|
92
|
+
if (rules.quote && value.includes(quoteChar)) {
|
|
93
|
+
// Needs ES2021
|
|
94
|
+
// value = value.replaceAll(quoteChar, quoteChar + quoteChar);
|
|
95
|
+
value = value.split(quoteChar).join(quoteChar + quoteChar);
|
|
96
|
+
needsQuoting = true;
|
|
97
|
+
}
|
|
98
|
+
// 4. Zeilenumbrüche → brauchen Quoting
|
|
99
|
+
if (rules.newline && (value.includes("\n") || value.includes("\r"))) {
|
|
100
|
+
needsQuoting = true;
|
|
101
|
+
}
|
|
102
|
+
// 5. Trennzeichen im Wert → braucht Quoting
|
|
103
|
+
if (rules.delimiter && value.includes(delimiter)) {
|
|
104
|
+
needsQuoting = true;
|
|
105
|
+
}
|
|
106
|
+
if (needsQuoting) {
|
|
107
|
+
return `${quoteChar}${value}${quoteChar}`;
|
|
108
|
+
}
|
|
109
|
+
return value;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Streamt Records zeilenweise als Generator (speicherschonend für große Dateien).
|
|
113
|
+
*/
|
|
114
|
+
*stream(records, columns) {
|
|
115
|
+
const { delimiter, lineEnding, writeHeader } = this.opts;
|
|
116
|
+
if (writeHeader) {
|
|
117
|
+
yield columns.map((h) => this.escapeField(h)).join(delimiter) + lineEnding;
|
|
118
|
+
}
|
|
119
|
+
for (const record of records) {
|
|
120
|
+
const row = columns.map((col) => {
|
|
121
|
+
const value = record[col];
|
|
122
|
+
if ((value === null || value === undefined) && this.opts.nullAsEmpty) {
|
|
123
|
+
return "";
|
|
124
|
+
}
|
|
125
|
+
return this.escapeField(String(value ?? ""));
|
|
126
|
+
});
|
|
127
|
+
yield row.join(delimiter) + lineEnding;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regeln, welche Zeichen escaped werden sollen.
|
|
3
|
+
*/
|
|
4
|
+
export interface EscapeRules {
|
|
5
|
+
/** Verdoppelt Anführungszeichen innerhalb von Feldern: " → "" */
|
|
6
|
+
quote?: boolean;
|
|
7
|
+
/** Felder mit Zeilenumbrüchen werden gequotet */
|
|
8
|
+
newline?: boolean;
|
|
9
|
+
/** Felder, die das Trennzeichen enthalten, werden gequotet */
|
|
10
|
+
delimiter?: boolean;
|
|
11
|
+
/** Backslashes werden escaped: \ → \\ */
|
|
12
|
+
backslash?: boolean;
|
|
13
|
+
/** Null-Bytes werden escaped: \0 → \\0 */
|
|
14
|
+
nullByte?: boolean;
|
|
15
|
+
}
|
|
16
|
+
export interface CSVParserOptions {
|
|
17
|
+
/** Trennzeichen (Standard: ",") */
|
|
18
|
+
delimiter?: string;
|
|
19
|
+
/** Anführungszeichen-Zeichen (Standard: '"') */
|
|
20
|
+
quoteChar?: string;
|
|
21
|
+
/** Escape-Zeichen für Backslash-Escaping (Standard: kein Backslash-Escape) */
|
|
22
|
+
escapeChar?: string;
|
|
23
|
+
/** Erste Zeile als Header interpretieren (Standard: true) */
|
|
24
|
+
hasHeader?: boolean;
|
|
25
|
+
/** Führende/nachfolgende Leerzeichen in Feldern trimmen (Standard: false) */
|
|
26
|
+
trimFields?: boolean;
|
|
27
|
+
/** Leere Zeilen überspringen (Standard: true) */
|
|
28
|
+
skipEmptyLines?: boolean;
|
|
29
|
+
/** Kommentare überspringen (z. B. "#") — leer = deaktiviert */
|
|
30
|
+
commentChar?: string;
|
|
31
|
+
}
|
|
32
|
+
export interface CSVStringifierOptions {
|
|
33
|
+
/** Trennzeichen (Standard: ",") */
|
|
34
|
+
delimiter?: string;
|
|
35
|
+
/** Anführungszeichen-Zeichen (Standard: '"') */
|
|
36
|
+
quoteChar?: string;
|
|
37
|
+
/** Zeilenende (Standard: "\r\n" per RFC 4180) */
|
|
38
|
+
lineEnding?: "\r\n" | "\n" | "\r";
|
|
39
|
+
/** Escape-Regeln */
|
|
40
|
+
escapeRules?: EscapeRules;
|
|
41
|
+
/** Alle Felder immer quoten, unabhängig vom Inhalt (Standard: false) */
|
|
42
|
+
alwaysQuote?: boolean;
|
|
43
|
+
/** Spaltenreihenfolge (Standard: Reihenfolge der Keys im ersten Objekt) */
|
|
44
|
+
columns?: string[];
|
|
45
|
+
/** Header-Zeile schreiben (Standard: true) */
|
|
46
|
+
writeHeader?: boolean;
|
|
47
|
+
/** Null / undefined als leeres Feld ausgeben (Standard: true) */
|
|
48
|
+
nullAsEmpty?: boolean;
|
|
49
|
+
}
|
|
50
|
+
export interface ParseResult<T = Record<string, string>> {
|
|
51
|
+
/** Geparste Datensätze als Array von Objekten */
|
|
52
|
+
records: T[];
|
|
53
|
+
/** Header-Zeile (falls hasHeader = true) */
|
|
54
|
+
headers: string[];
|
|
55
|
+
/** Rohe Zeilen als String-Arrays (inkl. Header) */
|
|
56
|
+
rawRows: string[][];
|
|
57
|
+
/** Warnungen (z. B. ungleich viele Felder) */
|
|
58
|
+
warnings: ParseWarning[];
|
|
59
|
+
}
|
|
60
|
+
export interface ParseWarning {
|
|
61
|
+
row: number;
|
|
62
|
+
message: string;
|
|
63
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/utils/index.d.ts
CHANGED
|
@@ -3,3 +3,5 @@ export type { ParseOptions as MarkdownParseOptions } from "./markdown.js";
|
|
|
3
3
|
export { parse as parseMarkdown, parseToDocument as parseMarkdownToDocument, exportcss as exportMarkdownCSS } from "./markdown.js";
|
|
4
4
|
export type { JSONValue } from "./jsonc-parser.js";
|
|
5
5
|
export { parseJSONC } from "./jsonc-parser.js";
|
|
6
|
+
export declare function hash(str: string): number;
|
|
7
|
+
export declare function shuffle<T>(array: T[]): T[];
|
package/dist/utils/index.js
CHANGED
|
@@ -3,3 +3,20 @@
|
|
|
3
3
|
export { clamp, lerp, map, scale } from "./math.js";
|
|
4
4
|
export { parse as parseMarkdown, parseToDocument as parseMarkdownToDocument, exportcss as exportMarkdownCSS } from "./markdown.js";
|
|
5
5
|
export { parseJSONC } from "./jsonc-parser.js";
|
|
6
|
+
// Tiny Hash function
|
|
7
|
+
export function hash(str) {
|
|
8
|
+
let h = 0;
|
|
9
|
+
for (let i = 0; i < str.length; i++) {
|
|
10
|
+
h = (h << 5) - h + str.charCodeAt(i);
|
|
11
|
+
h |= 0; // 32bit int
|
|
12
|
+
}
|
|
13
|
+
return h;
|
|
14
|
+
}
|
|
15
|
+
// Make an array random
|
|
16
|
+
export function shuffle(array) {
|
|
17
|
+
for (let i = array.length - 1; i > 0; i--) {
|
|
18
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
19
|
+
[array[i], array[j]] = [array[j], array[i]];
|
|
20
|
+
}
|
|
21
|
+
return array;
|
|
22
|
+
}
|
package/dist/utils/markdown.js
CHANGED
|
@@ -37,17 +37,17 @@ function renderInline(text, opts) {
|
|
|
37
37
|
// Escaped Zeichen sichern (wichtig für Markdown-Sonderzeichen)
|
|
38
38
|
// ---------------------------------------------------------------------
|
|
39
39
|
const ESCAPES = {
|
|
40
|
-
"\\\\": "
|
|
41
|
-
"\\[": "
|
|
42
|
-
"\\]": "
|
|
43
|
-
"\\(": "
|
|
44
|
-
"\\)": "
|
|
45
|
-
"\\|": "
|
|
46
|
-
'\\"': "
|
|
47
|
-
"\\*": "
|
|
48
|
-
"\\_": "
|
|
49
|
-
"\\`": "
|
|
50
|
-
"\\~": "
|
|
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
51
|
};
|
|
52
52
|
for (const [char, placeholder] of Object.entries(ESCAPES)) {
|
|
53
53
|
const regex = new RegExp(char.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&"), "g");
|
|
@@ -58,7 +58,7 @@ function renderInline(text, opts) {
|
|
|
58
58
|
if (!opts.sanitize) {
|
|
59
59
|
text = text.replace(/<[^>]+>/g, (match) => {
|
|
60
60
|
const idx = htmlPlaceholders.push(match) - 1;
|
|
61
|
-
return
|
|
61
|
+
return `@@HTML${idx}@@`;
|
|
62
62
|
});
|
|
63
63
|
}
|
|
64
64
|
// Links [text](url "title")
|
|
@@ -67,7 +67,7 @@ function renderInline(text, opts) {
|
|
|
67
67
|
const ext = opts.externalLinks && isExternalUrl(url)
|
|
68
68
|
? ' target="_blank" rel="noopener noreferrer"'
|
|
69
69
|
: "";
|
|
70
|
-
return `<a href="${url}"${t}${ext}>${
|
|
70
|
+
return `<a href="${url}"${t}${ext}>${linkText}</a>`;
|
|
71
71
|
});
|
|
72
72
|
// Code-Spans (höchste Priorität, vor allem anderen)
|
|
73
73
|
text = text.replace(/`{2}([^`]+)`{2}|`([^`\n]+)`/g, (_, a, b) => {
|
|
@@ -115,12 +115,19 @@ function renderInline(text, opts) {
|
|
|
115
115
|
}
|
|
116
116
|
// HTML-Platzhalter wiederherstellen
|
|
117
117
|
if (!opts.sanitize) {
|
|
118
|
-
text = text.replace(
|
|
119
|
-
}
|
|
120
|
-
// <<< HIER rein
|
|
121
|
-
for (const [char, placeholder] of Object.entries(ESCAPES)) {
|
|
122
|
-
text = text.replace(new RegExp(placeholder, "g"), char.replace("\\", ""));
|
|
118
|
+
text = text.replace(/@@HTML(\d+)@@/g, (_, i) => htmlPlaceholders[+i]);
|
|
123
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, "[");
|
|
124
131
|
return text;
|
|
125
132
|
}
|
|
126
133
|
function parseListItems(lines, baseIndent) {
|
|
@@ -197,9 +204,10 @@ function parseTable(block, opts) {
|
|
|
197
204
|
.filter((_, i, a) => !(i === 0 && _ === "") && !(i === a.length - 1 && _ === ""))
|
|
198
205
|
.map((c) => c
|
|
199
206
|
.trim()
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
207
|
+
// .replace(/\\\|/g, "|")
|
|
208
|
+
// .replace(/\\\[/g, "[")
|
|
209
|
+
// .replace(/\\\]/g, "]")
|
|
210
|
+
);
|
|
203
211
|
const alignRow = rows[1].split(/(?<!\\)\|/).filter((c) => /[-:]/.test(c));
|
|
204
212
|
const aligns = alignRow.map((c) => {
|
|
205
213
|
c = c.trim();
|
|
@@ -223,9 +231,10 @@ function parseTable(block, opts) {
|
|
|
223
231
|
.filter((_, i, a) => !(i === 0 && _ === "") && !(i === a.length - 1 && _ === ""))
|
|
224
232
|
.map((c) => c
|
|
225
233
|
.trim()
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
234
|
+
// .replace(/\\\|/g, "|")
|
|
235
|
+
// .replace(/\\\[/g, "[")
|
|
236
|
+
// .replace(/\\\]/g, "]")
|
|
237
|
+
);
|
|
229
238
|
return `<tr>\n${cells
|
|
230
239
|
.map((c, i) => {
|
|
231
240
|
const align = aligns[i] ? ` style="text-align:${aligns[i]}"` : "";
|
|
@@ -299,7 +308,7 @@ function parseBlocks(markdown, opts) {
|
|
|
299
308
|
// -----------------------------------------------------------------------
|
|
300
309
|
// Code-Block-Platzhalter (bereits in parse() extrahiert)
|
|
301
310
|
{
|
|
302
|
-
const m = remaining.match(
|
|
311
|
+
const m = remaining.match(/^@@CODEBLOCK\d+@@/);
|
|
303
312
|
if (m) {
|
|
304
313
|
output.push(m[0]); // wird später in parse() ersetzt
|
|
305
314
|
remaining = remaining.slice(m[0].length);
|
|
@@ -609,7 +618,7 @@ export function parse(markdown, options = {}) {
|
|
|
609
618
|
const codeBlockPlaceholders = [];
|
|
610
619
|
text = text.replace(/^(`{3,}|~{3,})([^\n]*)\n([\s\S]*?)\n?\1[ \t]*(?:\n|$)/gm, (_, fence, lang, code) => {
|
|
611
620
|
const idx = codeBlockPlaceholders.push(renderCodeBlock(lang.trim(), code)) - 1;
|
|
612
|
-
return
|
|
621
|
+
return `@@CODEBLOCK${idx}@@\n`;
|
|
613
622
|
});
|
|
614
623
|
// Fußnoten-Definitionen einsammeln
|
|
615
624
|
const { text: cleaned, notes } = collectFootnotes(text);
|
|
@@ -619,7 +628,7 @@ export function parse(markdown, options = {}) {
|
|
|
619
628
|
// Block-Parsing
|
|
620
629
|
let html = parseBlocks(text, opts);
|
|
621
630
|
// Platzhalter durch gerenderte Code-Blöcke ersetzen
|
|
622
|
-
html = html.replace(
|
|
631
|
+
html = html.replace(/@@CODEBLOCK(\d+)@@/g, (_, i) => codeBlockPlaceholders[+i]);
|
|
623
632
|
// Fußnoten-Liste anhängen
|
|
624
633
|
html += renderFootnoteList(notes, opts);
|
|
625
634
|
return html;
|
package/package.json
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "samengine",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.1",
|
|
4
4
|
"description": "A TypeScript game library to make HTML Games",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"files": [
|
|
7
7
|
"dist"
|
|
8
8
|
],
|
|
9
9
|
"scripts": {
|
|
10
|
+
"dev": "npx samengine-build",
|
|
10
11
|
"build": "tsc",
|
|
11
|
-
"pack": "node scripts/clean.js && npm run build && npm pack && git push --follow-tags &&
|
|
12
|
+
"pack": "node ../../scripts/clean.js && npm run build && npm pack && git push --follow-tags && npm run up",
|
|
12
13
|
"test": "bun test"
|
|
13
14
|
},
|
|
14
15
|
"exports": {
|
|
@@ -16,8 +17,11 @@
|
|
|
16
17
|
"./types": "./dist/types/index.js",
|
|
17
18
|
"./sound": "./dist/sound/index.js",
|
|
18
19
|
"./utils": "./dist/utils/index.js",
|
|
20
|
+
"./utils/csv": "./dist/utils/csv/index.js",
|
|
19
21
|
"./build": "./dist/build/index.js",
|
|
20
|
-
"./physics": "./dist/physics/index.js"
|
|
22
|
+
"./physics": "./dist/physics/index.js",
|
|
23
|
+
"./samegui": "./dist/samegui/index.js",
|
|
24
|
+
"./storage": "./dist/storage/index.js"
|
|
21
25
|
},
|
|
22
26
|
"keywords": [
|
|
23
27
|
"game",
|
|
@@ -27,8 +31,9 @@
|
|
|
27
31
|
"license": "MIT",
|
|
28
32
|
"devDependencies": {
|
|
29
33
|
"chalk": "^5.6.2",
|
|
30
|
-
"@types/node": "^25.
|
|
31
|
-
"typescript": "^6.0.
|
|
34
|
+
"@types/node": "^25.6.0",
|
|
35
|
+
"typescript": "^6.0.3",
|
|
36
|
+
"automatic-md-index": "^1.3.6"
|
|
32
37
|
},
|
|
33
38
|
"repository": {
|
|
34
39
|
"type": "git",
|
package/dist/build/index.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function version(): string;
|
package/dist/build/index.js
DELETED