glslh 1.1.1 → 1.2.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/CHANGELOG.md +17 -1
- package/README.md +25 -24
- package/package.json +5 -3
- package/src/derived.js +73 -15
- package/src/dom-canvas.js +91 -54
- package/src/expr.js +55 -7
- package/src/loader.js +12 -1
- package/src/parser.js +6 -97
- package/src/validator.js +104 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,22 @@
|
|
|
1
1
|
## 📋 `glslh` Changelog
|
|
2
2
|
|
|
3
|
-
### **v1.
|
|
3
|
+
### **v1.2.0** (Latest)
|
|
4
|
+
|
|
5
|
+
- **Topological Sorting for Derived Uniforms**: Upgraded `buildDerived` in `derived.js` to implement Kahn's algorithm for dependency graphs, ensuring derived uniform expressions evaluate in the correct order regardless of JSON declaration placement and throwing immediate errors on circular dependencies.
|
|
6
|
+
- **Enhanced Expression Engine & Math Utilities**: Fixed operator precedence for exponentiation (`**`) with correct right-associativity, and introduced software implementations of essential GLSL math functions (`mix`, `smoothstep`, `clamp`, `fract`, `step`, `mod`, `length`, `distance`, `dot`) for CPU-side derived uniform evaluations. Secured variable resolution using `Object.hasOwn()`.
|
|
7
|
+
- **Overload-Aware Static Collision Detection**: Refactored `detectFunctionCollisions` in `parser.js` to support valid GLSL function overloading (distinguishing functions by parameter signatures) and filter out forward declaration prototypes ending in semicolons (`;`).
|
|
8
|
+
- **DOM Canvas Lifecycle & Mouse Tracking**: Enhanced `GLSLHDomCanvas` with real-time normalized/pixel mouse coordinate tracking (`mouseX`, `mouseY`), fixed Three.js clock delta calculation order, and added resource cleanup (`dispose()`) for geometries, materials, and WebGL contexts.
|
|
9
|
+
- **Native Version Pipeline Integration**: Streamlined `#version` handling by removing forced string injections and delegating version management natively to Three.js via `glslVersion: THREE.GLSL3`, completely eliminating WebGL driver compilation clashes.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
### **v1.1.2**
|
|
14
|
+
|
|
15
|
+
- **AI-Native Error Context Mapping**: Upgraded `translateShaderError` inside `parser.js` to automatically extract and embed surrounding 5-line code snippets (`--- Code Context ---`) directly alongside modular file and line indicators, allowing developers and AI assistants to diagnose and patch WebGL compilation errors instantly without manual file lookups.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
### **v1.1.1**
|
|
4
20
|
|
|
5
21
|
- **Documentation & README Refinement**: Updated documentation, architecture diagrams, and release logs to reflect complete structural guides for modular struct-based composition, CPU/GPU division of labor, and extension workflows.
|
|
6
22
|
|
package/README.md
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
[](https://opensource.org/licenses/MIT)
|
|
6
6
|
[](https://developer.mozilla.org/en-US/docs/Web/JavaScript)
|
|
7
7
|
[](https://threejs.org/)
|
|
8
|
+
[](https://www.npmjs.com/package/glslh)
|
|
8
9
|
|
|
9
10
|
---
|
|
10
11
|
|
|
@@ -12,24 +13,22 @@
|
|
|
12
13
|
|
|
13
14
|
`glslh` establishes a clean pipeline separating CPU-side orchestration from GPU-side parallel rendering:
|
|
14
15
|
|
|
15
|
-
```
|
|
16
|
-
|
|
16
|
+
```text
|
|
17
17
|
.glslh (Self-Contained Header)
|
|
18
18
|
┌─────────────────────────────┐
|
|
19
|
-
│
|
|
20
|
-
│
|
|
21
|
-
│
|
|
19
|
+
│ /* @uniforms { ... } */ │ ──► [CPU] Auto-linked JS-to-GLSL Uniforms
|
|
20
|
+
│ #include "utils/noise.glsl"│ ──► [CPU] Recursive File & Dependency Resolution
|
|
21
|
+
│ Math.PI macro injection │ ──► [CPU] JIT Constant & Topological Expression Evaluator
|
|
22
22
|
└──────────────┬──────────────┘
|
|
23
|
-
▼
|
|
24
|
-
[ glslh Preprocessor ]
|
|
25
|
-
▼
|
|
23
|
+
▼
|
|
24
|
+
[ glslh Preprocessor ]
|
|
25
|
+
▼
|
|
26
26
|
┌─────────────────────────────┐
|
|
27
|
-
│
|
|
27
|
+
│ Final Pure GLSL String │ ──► [GPU] Compiled & Executed via WebGL
|
|
28
28
|
└─────────────────────────────┘
|
|
29
|
-
|
|
30
29
|
```
|
|
31
30
|
|
|
32
|
-
- **CPU-Side (JavaScript):** File fetching, `#include` stitching, uniform metadata parsing, constant macro evaluation (`Math.PI`, `Math.TAU`), function collision checking, error line mapping, and real-time
|
|
31
|
+
- **CPU-Side (JavaScript):** File fetching, `#include` stitching, uniform metadata parsing, constant macro evaluation (`Math.PI`, `Math.TAU`), topological sorting for derived uniform expressions, function collision checking, error line mapping, and real-time animation loops (`requestAnimationFrame`).
|
|
33
32
|
- **GPU-Side (WebGL / GLSL):** Pure parallel rasterization, vertex positioning, and fragment color blending executing simultaneously across graphics hardware cores.
|
|
34
33
|
|
|
35
34
|
---
|
|
@@ -39,12 +38,13 @@
|
|
|
39
38
|
- **Clear CPU/GPU Separation:** All orchestration, math constant calculations (`Math.PI`), and uniform state updates run efficiently on the **CPU**, while heavy rasterization and pixel shading run natively in parallel on the **GPU**.
|
|
40
39
|
- **Zero Runtime Performance Cost:** Because modular files are stitched and compiled once during initialization, your shaders execute at full native GPU speed with zero runtime overhead.
|
|
41
40
|
- **C-Style `#include` Directives:** Split massive shaders into modular files with full circular dependency protection and strict extension validation.
|
|
42
|
-
- **
|
|
43
|
-
- **
|
|
41
|
+
- **Topological Derived Uniforms:** Chain derived expressions together in `@uniforms` (e.g., variable `u_b` depending on `u_a`) with automated Kahn's algorithm sorting and circular dependency detection.
|
|
42
|
+
- **Overload-Aware Static Collision Detection:** Automatically scans included modules and warns you if conflicting function signatures clash, while fully supporting valid GLSL function overloading distinguished by parameter signatures.
|
|
43
|
+
- **AI-Friendly Error Context Mapping:** Automatically captures and embeds 5-line surrounding code context snippets alongside modular file and line numbers when WebGL compilation fails.
|
|
44
44
|
- **Self-Contained Metadata Blocks:** Define uniform data types, default values, and derived per-frame formulas right at the top of your shader file (`@uniforms`).
|
|
45
45
|
- **Direct JavaScript Math Injection:** Use native JS constants like `Math.PI` or `Math.TAU` directly in your GLSL code; the preprocessor evaluates them before GPU compilation.
|
|
46
46
|
- **Framework Agnostic:** Seamlessly output standard Three.js `ShaderMaterial` objects ready for Vanilla JS, Three.js, or React Three Fiber.
|
|
47
|
-
- **HTML Declarative Support:** Spin up shaders instantly using HTML data attributes (`data-fragment-url="..."`).
|
|
47
|
+
- **HTML Declarative Support & Cleanup:** Spin up shaders instantly using HTML data attributes (`data-fragment-url="..."`) with full canvas disposal and mouse tracking (`mouseX`, `mouseY`).
|
|
48
48
|
|
|
49
49
|
---
|
|
50
50
|
|
|
@@ -57,7 +57,8 @@ The metadata block is an optional JSON declaration placed at the very top of you
|
|
|
57
57
|
{
|
|
58
58
|
"u_intensity": { "type": "f", "value": 1.0 },
|
|
59
59
|
"u_color": { "type": "c", "value": [1.0, 0.4, 0.0] },
|
|
60
|
-
"
|
|
60
|
+
"u_speed": { "type": "f", "expr": "u_time * 2.0" },
|
|
61
|
+
"u_wave": { "type": "f", "expr": "sin(u_speed) * 0.5" }
|
|
61
62
|
}
|
|
62
63
|
*/
|
|
63
64
|
|
|
@@ -65,7 +66,7 @@ The metadata block is an optional JSON declaration placed at the very top of you
|
|
|
65
66
|
|
|
66
67
|
### Why use it?
|
|
67
68
|
|
|
68
|
-
In standard WebGL and Three.js, wiring JavaScript variables to shader uniforms requires tedious boilerplate. The `@uniforms` block decentralizes this configuration, letting your shader file declare its own parameters and derived per-frame expressions so `glslh` can auto-bind and update them instantly.
|
|
69
|
+
In standard WebGL and Three.js, wiring JavaScript variables to shader uniforms requires tedious boilerplate. The `@uniforms` block decentralizes this configuration, letting your shader file declare its own parameters and derived per-frame expressions (with automatic topological dependency sorting) so `glslh` can auto-bind and update them instantly.
|
|
69
70
|
|
|
70
71
|
### What if you forget it?
|
|
71
72
|
|
|
@@ -80,11 +81,11 @@ package/
|
|
|
80
81
|
├── package.json
|
|
81
82
|
├── README.md
|
|
82
83
|
└── src/
|
|
83
|
-
├── parser.js
|
|
84
|
-
├── loader.js
|
|
85
|
-
├── dom-canvas.js
|
|
86
|
-
├── expr.js
|
|
87
|
-
└── derived.js
|
|
84
|
+
├── parser.js # C-style #include preprocessor, path resolution, collision check & error mapper
|
|
85
|
+
├── loader.js # Metadata JSON parser & Three.js uniform binder
|
|
86
|
+
├── dom-canvas.js # HTML data-attribute and canvas auto-loader wrapper with mouse tracking & dispose()
|
|
87
|
+
├── expr.js # Safe, CSP-compliant AST-based JIT expression evaluator with custom math helpers
|
|
88
|
+
└── derived.js # Topological sort & per-frame derived uniform state manager
|
|
88
89
|
|
|
89
90
|
```
|
|
90
91
|
|
|
@@ -93,7 +94,7 @@ package/
|
|
|
93
94
|
## 🚀 Installation & Quick Start
|
|
94
95
|
|
|
95
96
|
```bash
|
|
96
|
-
npm install glslh@1.
|
|
97
|
+
npm install glslh@1.2.0
|
|
97
98
|
```
|
|
98
99
|
|
|
99
100
|
### 1. Create a `.glslh` Shader File (`./shaders/header.glslh`)
|
|
@@ -104,7 +105,7 @@ npm install glslh@1.1.1
|
|
|
104
105
|
|
|
105
106
|
## ⚡ VS Code Extension (Live Preview)
|
|
106
107
|
|
|
107
|
-
Want instant syntax highlighting, color pickers, and a live WebGL preview pane right inside your editor? Install the official [
|
|
108
|
+
Want instant syntax highlighting, color pickers, and a live WebGL preview pane right inside your editor? Install the official **[GLSLH Previewer](https://marketplace.visualstudio.com/items?itemName=SudhanshuAmbastha.glslh-previewer)** extension from the marketplace:
|
|
108
109
|
|
|
109
110
|
- **Live WebGL Preview:** Inspect shaders side-by-side with automatic hot-reloading on save.
|
|
110
111
|
- **Native Color Pickers:** Click color definitions or uniform arrays in `@uniforms` or `vec3()` blocks to adjust values visually.
|
|
@@ -114,4 +115,4 @@ Want instant syntax highlighting, color pickers, and a live WebGL preview pane r
|
|
|
114
115
|
|
|
115
116
|
## 📜 License
|
|
116
117
|
|
|
117
|
-
Distributed under the **MIT License**. See [`LICENSE`](
|
|
118
|
+
Distributed under the **MIT License**. See [`LICENSE`](https://www.google.com/search?q=../LICENSE) for more information.
|
package/package.json
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "glslh",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "A lightweight GLSL preprocessor and runtime bridge for modular shader development.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/dom-canvas.js",
|
|
7
7
|
"exports": {
|
|
8
8
|
".": "./src/dom-canvas.js",
|
|
9
9
|
"./parser": "./src/parser.js",
|
|
10
|
-
"./loader": "./src/loader.js"
|
|
10
|
+
"./loader": "./src/loader.js",
|
|
11
|
+
"./validator": "./src/validator.js",
|
|
12
|
+
"./src/*": "./src/*"
|
|
11
13
|
},
|
|
12
14
|
"repository": {
|
|
13
15
|
"type": "git",
|
|
@@ -17,7 +19,6 @@
|
|
|
17
19
|
"url": "https://github.com/Sudhanshu-Ambastha/glsl-header/issues"
|
|
18
20
|
},
|
|
19
21
|
"homepage": "https://github.com/Sudhanshu-Ambastha/glsl-header#readme",
|
|
20
|
-
"logo": "./assets/glsl_header.svg",
|
|
21
22
|
"keywords": [
|
|
22
23
|
"glsl",
|
|
23
24
|
"shaders",
|
|
@@ -29,6 +30,7 @@
|
|
|
29
30
|
],
|
|
30
31
|
"author": "Sudhanshu Ambastha",
|
|
31
32
|
"license": "MIT",
|
|
33
|
+
"sideEffects": false,
|
|
32
34
|
"peerDependencies": {
|
|
33
35
|
"three": "^0.150.0 || ^0.160.0"
|
|
34
36
|
}
|
package/src/derived.js
CHANGED
|
@@ -1,11 +1,60 @@
|
|
|
1
1
|
import { compileExpr } from "./expr.js";
|
|
2
2
|
|
|
3
3
|
export function buildDerived(defs) {
|
|
4
|
-
const items =
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
const items = [];
|
|
5
|
+
const graph = new Map();
|
|
6
|
+
const inDegree = new Map();
|
|
7
|
+
const defsMap = new Map();
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
for (const [name, d] of Object.entries(defs)) {
|
|
10
|
+
if (d && typeof d.expr === "string") {
|
|
11
|
+
const compiled = compileExpr(d.expr);
|
|
12
|
+
defsMap.set(name, { name, ...compiled });
|
|
13
|
+
graph.set(name, new Set());
|
|
14
|
+
inDegree.set(name, 0);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
for (const [name, item] of defsMap) {
|
|
19
|
+
for (const dep of item.deps) {
|
|
20
|
+
if (defsMap.has(dep)) {
|
|
21
|
+
if (!graph.has(dep)) graph.set(dep, new Set());
|
|
22
|
+
if (!graph.get(dep).has(name)) {
|
|
23
|
+
graph.get(dep).add(name);
|
|
24
|
+
inDegree.set(name, (inDegree.get(name) || 0) + 1);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const queue = [];
|
|
31
|
+
for (const [name, degree] of inDegree) {
|
|
32
|
+
if (degree === 0) queue.push(name);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const sortedNames = [];
|
|
36
|
+
while (queue.length > 0) {
|
|
37
|
+
const curr = queue.shift();
|
|
38
|
+
sortedNames.push(curr);
|
|
39
|
+
|
|
40
|
+
const neighbors = graph.get(curr) || new Set();
|
|
41
|
+
for (const neighbor of neighbors) {
|
|
42
|
+
inDegree.set(neighbor, inDegree.get(neighbor) - 1);
|
|
43
|
+
if (inDegree.get(neighbor) === 0) {
|
|
44
|
+
queue.push(neighbor);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (sortedNames.length !== defsMap.size) {
|
|
50
|
+
throw new Error(
|
|
51
|
+
"GLSLH Dependency Error: Circular dependency detected in derived @uniforms expressions!",
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
for (const name of sortedNames) {
|
|
56
|
+
items.push(defsMap.get(name));
|
|
57
|
+
}
|
|
9
58
|
|
|
10
59
|
return function update(
|
|
11
60
|
uniforms,
|
|
@@ -22,30 +71,39 @@ export function buildDerived(defs) {
|
|
|
22
71
|
};
|
|
23
72
|
|
|
24
73
|
for (const [n, u] of Object.entries(uniforms)) {
|
|
25
|
-
if (
|
|
26
|
-
|
|
74
|
+
if (u && u.value !== undefined) {
|
|
75
|
+
if (typeof u.value === "number") {
|
|
76
|
+
ctx[n] = u.value;
|
|
77
|
+
} else if (
|
|
78
|
+
Array.isArray(u.value) ||
|
|
79
|
+
u.value.isVector2 ||
|
|
80
|
+
u.value.isVector3 ||
|
|
81
|
+
u.value.isVector4
|
|
82
|
+
) {
|
|
83
|
+
ctx[n] = u.value;
|
|
84
|
+
}
|
|
27
85
|
}
|
|
28
86
|
}
|
|
29
87
|
|
|
30
88
|
for (const it of items) {
|
|
31
|
-
if (!first && it.deps.length === 0) continue;
|
|
32
|
-
|
|
33
89
|
const v = it.run(ctx);
|
|
34
90
|
const u = uniforms[it.name];
|
|
35
91
|
|
|
36
|
-
if (!u) continue;
|
|
92
|
+
if (!u || u.value === undefined) continue;
|
|
37
93
|
|
|
38
|
-
if (
|
|
94
|
+
if (u.value.isColor && Array.isArray(v)) {
|
|
95
|
+
u.value.setRGB(v[0], v[1], v[2]);
|
|
96
|
+
} else if (Array.isArray(v) && typeof u.value.set === "function") {
|
|
39
97
|
u.value.set(...v);
|
|
98
|
+
} else if (typeof v === "number" && typeof u.value === "number") {
|
|
99
|
+
u.value = v;
|
|
100
|
+
} else if (typeof u.value.set === "function" && typeof v === "number") {
|
|
101
|
+
u.value.set(v);
|
|
40
102
|
} else {
|
|
41
103
|
u.value = v;
|
|
42
104
|
}
|
|
43
105
|
|
|
44
|
-
|
|
45
|
-
ctx[it.name] = v;
|
|
46
|
-
}
|
|
106
|
+
ctx[it.name] = v;
|
|
47
107
|
}
|
|
48
|
-
|
|
49
|
-
first = false;
|
|
50
108
|
};
|
|
51
109
|
}
|
package/src/dom-canvas.js
CHANGED
|
@@ -8,80 +8,98 @@ export class GLSLHDomCanvas {
|
|
|
8
8
|
this.width = Number.parseInt(this.canvas.getAttribute("width"), 10) || 600;
|
|
9
9
|
this.height =
|
|
10
10
|
Number.parseInt(this.canvas.getAttribute("height"), 10) || 400;
|
|
11
|
+
|
|
12
|
+
this.ready = false;
|
|
13
|
+
this.error = null;
|
|
14
|
+
this.animationId = null;
|
|
15
|
+
this.mouseX = 0.0;
|
|
16
|
+
this.mouseY = 0.0;
|
|
17
|
+
|
|
11
18
|
this.init();
|
|
19
|
+
this.bindEvents();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
bindEvents() {
|
|
23
|
+
this.onMouseMove = (e) => {
|
|
24
|
+
const rect = this.canvas.getBoundingClientRect();
|
|
25
|
+
this.mouseX = e.clientX - rect.left;
|
|
26
|
+
// Invert Y to match standard GLSL coordinate conventions (bottom-left origin)
|
|
27
|
+
this.mouseY = rect.height - (e.clientY - rect.top);
|
|
28
|
+
};
|
|
29
|
+
this.canvas.addEventListener("mousemove", this.onMouseMove);
|
|
12
30
|
}
|
|
13
31
|
|
|
14
32
|
async init() {
|
|
15
33
|
const fragUrl = this.canvas.getAttribute("data-fragment-url");
|
|
16
34
|
|
|
17
35
|
console.log("GLSLH: Initializing WebGL Renderer...");
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
this.scene = new THREE.Scene();
|
|
25
|
-
this.camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
|
|
26
|
-
|
|
27
|
-
const vertexShader = `
|
|
28
|
-
void main() {
|
|
29
|
-
gl_Position = vec4(position, 1.0);
|
|
30
|
-
}
|
|
31
|
-
`;
|
|
36
|
+
try {
|
|
37
|
+
this.renderer = new THREE.WebGLRenderer({
|
|
38
|
+
canvas: this.canvas,
|
|
39
|
+
antialias: true,
|
|
40
|
+
});
|
|
41
|
+
this.renderer.setSize(this.width, this.height, false);
|
|
32
42
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
43
|
+
this.scene = new THREE.Scene();
|
|
44
|
+
this.camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
|
|
45
|
+
|
|
46
|
+
const vertexShader = `
|
|
47
|
+
void main() {
|
|
48
|
+
gl_Position = vec4(position, 1.0);
|
|
49
|
+
}
|
|
50
|
+
`;
|
|
51
|
+
|
|
52
|
+
let fragmentShader = "";
|
|
53
|
+
let uniforms = {};
|
|
54
|
+
let shaderModule = null;
|
|
36
55
|
|
|
37
|
-
try {
|
|
38
56
|
console.log(`GLSLH: Fetching and parsing shader module -> ${fragUrl}`);
|
|
39
57
|
shaderModule = await this.loader.loadShaderModule(fragUrl);
|
|
40
58
|
console.log("GLSLH: Shader successfully parsed!", shaderModule);
|
|
41
59
|
|
|
42
60
|
fragmentShader = shaderModule.source;
|
|
43
61
|
uniforms = shaderModule.uniforms;
|
|
44
|
-
} catch (err) {
|
|
45
|
-
console.error("❌ GLSLH Pipeline Error:", err);
|
|
46
|
-
fragmentShader = `
|
|
47
|
-
void main() {
|
|
48
|
-
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
|
|
49
|
-
}
|
|
50
|
-
`;
|
|
51
|
-
}
|
|
52
62
|
|
|
53
|
-
|
|
54
|
-
|
|
63
|
+
this.shaderModule = shaderModule;
|
|
64
|
+
this.uniforms = uniforms;
|
|
55
65
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
66
|
+
if (this.uniforms.u_resolution && !this.uniforms.u_resolution.value) {
|
|
67
|
+
this.uniforms.u_resolution.value = new THREE.Vector2(
|
|
68
|
+
this.width,
|
|
69
|
+
this.height,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
if (this.uniforms.u_time && this.uniforms.u_time.value === undefined) {
|
|
73
|
+
this.uniforms.u_time.value = 0.0;
|
|
74
|
+
}
|
|
65
75
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
76
|
+
this.material = new THREE.ShaderMaterial({
|
|
77
|
+
vertexShader,
|
|
78
|
+
fragmentShader,
|
|
79
|
+
uniforms: this.uniforms,
|
|
80
|
+
glslVersion: THREE.GLSL3,
|
|
81
|
+
});
|
|
71
82
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
83
|
+
const geometry = new THREE.PlaneGeometry(2, 2);
|
|
84
|
+
this.mesh = new THREE.Mesh(geometry, this.material);
|
|
85
|
+
this.scene.add(this.mesh);
|
|
75
86
|
|
|
76
|
-
|
|
77
|
-
|
|
87
|
+
this.clock = new THREE.Clock();
|
|
88
|
+
this.ready = true;
|
|
89
|
+
this.animate();
|
|
90
|
+
} catch (err) {
|
|
91
|
+
this.error = err;
|
|
92
|
+
console.error("❌ GLSLH Pipeline Error:", err);
|
|
93
|
+
}
|
|
78
94
|
}
|
|
79
95
|
|
|
80
96
|
animate() {
|
|
81
|
-
|
|
97
|
+
if (!this.ready) return;
|
|
98
|
+
this.animationId = requestAnimationFrame(() => this.animate());
|
|
82
99
|
|
|
83
|
-
|
|
100
|
+
// Fix: Call getDelta() first to update the clock, then read elapsed time
|
|
84
101
|
const delta = this.clock.getDelta();
|
|
102
|
+
const elapsedTime = this.clock.elapsedTime;
|
|
85
103
|
|
|
86
104
|
if (this.uniforms.u_time) {
|
|
87
105
|
this.uniforms.u_time.value = elapsedTime;
|
|
@@ -89,6 +107,9 @@ export class GLSLHDomCanvas {
|
|
|
89
107
|
if (this.uniforms.u_delta) {
|
|
90
108
|
this.uniforms.u_delta.value = delta;
|
|
91
109
|
}
|
|
110
|
+
if (this.uniforms.u_mouse) {
|
|
111
|
+
this.uniforms.u_mouse.value.set(this.mouseX, this.mouseY);
|
|
112
|
+
}
|
|
92
113
|
|
|
93
114
|
if (this.shaderModule && typeof this.shaderModule.update === "function") {
|
|
94
115
|
try {
|
|
@@ -97,8 +118,8 @@ export class GLSLHDomCanvas {
|
|
|
97
118
|
delta: delta,
|
|
98
119
|
width: this.width,
|
|
99
120
|
height: this.height,
|
|
100
|
-
mouseX:
|
|
101
|
-
mouseY:
|
|
121
|
+
mouseX: this.mouseX,
|
|
122
|
+
mouseY: this.mouseY,
|
|
102
123
|
});
|
|
103
124
|
} catch (evalErr) {
|
|
104
125
|
console.error(
|
|
@@ -110,10 +131,26 @@ export class GLSLHDomCanvas {
|
|
|
110
131
|
|
|
111
132
|
this.renderer.render(this.scene, this.camera);
|
|
112
133
|
}
|
|
134
|
+
|
|
135
|
+
dispose() {
|
|
136
|
+
if (this.animationId) {
|
|
137
|
+
cancelAnimationFrame(this.animationId);
|
|
138
|
+
}
|
|
139
|
+
if (this.canvas && this.onMouseMove) {
|
|
140
|
+
this.canvas.removeEventListener("mousemove", this.onMouseMove);
|
|
141
|
+
}
|
|
142
|
+
this.mesh?.geometry?.dispose();
|
|
143
|
+
this.material?.dispose();
|
|
144
|
+
this.renderer?.dispose();
|
|
145
|
+
this.ready = false;
|
|
146
|
+
console.log("GLSLH: Canvas instance disposed cleanly.");
|
|
147
|
+
}
|
|
113
148
|
}
|
|
114
149
|
|
|
115
150
|
export function autoInitGlslCanvases(selector = "canvas.glsl-canvas") {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
.
|
|
151
|
+
const instances = [];
|
|
152
|
+
document.querySelectorAll(selector).forEach((canvas) => {
|
|
153
|
+
instances.push(new GLSLHDomCanvas(canvas));
|
|
154
|
+
});
|
|
155
|
+
return instances;
|
|
119
156
|
}
|
package/src/expr.js
CHANGED
|
@@ -18,8 +18,44 @@ const FUNCS = new Set([
|
|
|
18
18
|
"round",
|
|
19
19
|
"sign",
|
|
20
20
|
"hypot",
|
|
21
|
+
// Shader & procedural utility additions
|
|
22
|
+
"clamp",
|
|
23
|
+
"mix",
|
|
24
|
+
"smoothstep",
|
|
25
|
+
"step",
|
|
26
|
+
"mod",
|
|
27
|
+
"length",
|
|
28
|
+
"distance",
|
|
29
|
+
"dot",
|
|
30
|
+
"fract",
|
|
21
31
|
]);
|
|
22
32
|
|
|
33
|
+
// Custom implementations for useful GLSL math functions not natively on Math
|
|
34
|
+
const SHADER_MATH = {
|
|
35
|
+
clamp: (x, min, max) => Math.min(Math.max(x, min), max),
|
|
36
|
+
mix: (a, b, t) => a * (1 - t) + b * t,
|
|
37
|
+
smoothstep: (edge0, edge1, x) => {
|
|
38
|
+
const t = Math.max(0, Math.min(1, (x - edge0) / (edge1 - edge0)));
|
|
39
|
+
return t * t * (3 - 2 * t);
|
|
40
|
+
},
|
|
41
|
+
step: (edge, x) => (x < edge ? 0 : 1),
|
|
42
|
+
mod: (x, y) => x - y * Math.floor(x / y),
|
|
43
|
+
length: (arr) => (Array.isArray(arr) ? Math.hypot(...arr) : Math.abs(arr)),
|
|
44
|
+
distance: (a, b) => {
|
|
45
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
46
|
+
return Math.hypot(...a.map((v, i) => v - b[i]));
|
|
47
|
+
}
|
|
48
|
+
return Math.abs(a - b);
|
|
49
|
+
},
|
|
50
|
+
dot: (a, b) => {
|
|
51
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
52
|
+
return a.reduce((sum, v, i) => sum + v * b[i], 0);
|
|
53
|
+
}
|
|
54
|
+
return a * b;
|
|
55
|
+
},
|
|
56
|
+
fract: (x) => x - Math.floor(x),
|
|
57
|
+
};
|
|
58
|
+
|
|
23
59
|
const CONSTS = {
|
|
24
60
|
PI: Math.PI,
|
|
25
61
|
TAU: Math.PI * 2,
|
|
@@ -105,6 +141,10 @@ export function compileExpr(src) {
|
|
|
105
141
|
if (eat("(")) {
|
|
106
142
|
if (!FUNCS.has(name)) throw new Error(`Function not allowed: ${t.id}`);
|
|
107
143
|
const args = list(")");
|
|
144
|
+
|
|
145
|
+
if (SHADER_MATH[name]) {
|
|
146
|
+
return (c) => SHADER_MATH[name](...args.map((f) => f(c)));
|
|
147
|
+
}
|
|
108
148
|
return (c) => Math[name](...args.map((f) => f(c)));
|
|
109
149
|
}
|
|
110
150
|
|
|
@@ -115,7 +155,7 @@ export function compileExpr(src) {
|
|
|
115
155
|
|
|
116
156
|
deps.add(name);
|
|
117
157
|
return (c) => {
|
|
118
|
-
if (!(name
|
|
158
|
+
if (!Object.hasOwn(c, name))
|
|
119
159
|
throw new Error(`Unknown variable "${name}" in "${src}"`);
|
|
120
160
|
return c[name];
|
|
121
161
|
};
|
|
@@ -123,18 +163,22 @@ export function compileExpr(src) {
|
|
|
123
163
|
throw new Error(`Unexpected '${t.op}' in "${src}"`);
|
|
124
164
|
}
|
|
125
165
|
|
|
166
|
+
function power() {
|
|
167
|
+
const base = primary();
|
|
168
|
+
if (eat("**")) {
|
|
169
|
+
const exp = power();
|
|
170
|
+
return (c) => base(c) ** exp(c);
|
|
171
|
+
}
|
|
172
|
+
return base;
|
|
173
|
+
}
|
|
174
|
+
|
|
126
175
|
function unary() {
|
|
127
176
|
if (eat("-")) {
|
|
128
177
|
const a = unary();
|
|
129
178
|
return (c) => -a(c);
|
|
130
179
|
}
|
|
131
180
|
eat("+");
|
|
132
|
-
|
|
133
|
-
if (eat("**")) {
|
|
134
|
-
const e = unary();
|
|
135
|
-
return (c) => b(c) ** e(c);
|
|
136
|
-
}
|
|
137
|
-
return b;
|
|
181
|
+
return power();
|
|
138
182
|
}
|
|
139
183
|
|
|
140
184
|
const term = binary(unary, ["*", "/", "%"]);
|
|
@@ -144,3 +188,7 @@ export function compileExpr(src) {
|
|
|
144
188
|
if (i !== toks.length) throw new Error(`Unexpected token in "${src}"`);
|
|
145
189
|
return { run, deps: [...deps] };
|
|
146
190
|
}
|
|
191
|
+
|
|
192
|
+
export function parseExpression(src) {
|
|
193
|
+
return compileExpr(src);
|
|
194
|
+
}
|
package/src/loader.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
import * as THREE from "three";
|
|
3
|
-
import { GLSLHParser } from "./parser.js";
|
|
3
|
+
import { GLSLHParser, BUILTINS } from "./parser.js";
|
|
4
4
|
import { buildDerived } from "./derived.js";
|
|
5
5
|
|
|
6
6
|
const ZERO_DEFAULTS = {
|
|
@@ -21,6 +21,16 @@ export class GLSLHLoader {
|
|
|
21
21
|
async loadShaderModule(filePath) {
|
|
22
22
|
const rawSource = await this.parser.fileResolver(filePath);
|
|
23
23
|
const parsed = await this.parser.parse(rawSource, filePath);
|
|
24
|
+
|
|
25
|
+
for (const [builtinName, builtinInfo] of Object.entries(BUILTINS)) {
|
|
26
|
+
if (!parsed.uniforms[builtinName]) {
|
|
27
|
+
parsed.uniforms[builtinName] = {
|
|
28
|
+
type: builtinInfo.type,
|
|
29
|
+
builtin: true,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
24
34
|
const threeUniforms = this.buildThreeUniforms(parsed.uniforms);
|
|
25
35
|
const updateDerived = buildDerived(parsed.uniforms);
|
|
26
36
|
|
|
@@ -34,6 +44,7 @@ export class GLSLHLoader {
|
|
|
34
44
|
vertexShader: vertexShaderSource,
|
|
35
45
|
fragmentShader: parsed.source,
|
|
36
46
|
uniforms: threeUniforms,
|
|
47
|
+
glslVersion: THREE.GLSL3,
|
|
37
48
|
...extraOptions,
|
|
38
49
|
});
|
|
39
50
|
},
|
package/src/parser.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { detectFunctionCollisions } from "./validator.js";
|
|
2
|
+
|
|
1
3
|
export const BUILTINS = {
|
|
2
4
|
u_time: { type: "f", from: (f) => f.time },
|
|
3
5
|
u_delta: { type: "f", from: (f) => f.delta },
|
|
@@ -103,7 +105,6 @@ export class GLSLHParser {
|
|
|
103
105
|
new Set(),
|
|
104
106
|
);
|
|
105
107
|
|
|
106
|
-
// Run duplicate function collision check on the final flattened source
|
|
107
108
|
detectFunctionCollisions(result.source);
|
|
108
109
|
|
|
109
110
|
return result;
|
|
@@ -239,15 +240,10 @@ export class GLSLHParser {
|
|
|
239
240
|
}
|
|
240
241
|
}
|
|
241
242
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
if (versionMatch) {
|
|
246
|
-
versionHeader = versionMatch[0];
|
|
247
|
-
processed = processed.slice(versionHeader.length);
|
|
248
|
-
}
|
|
243
|
+
const versionRegex = /\s*#version\s+[^\r\n]+(?:\r?\n)?/g;
|
|
244
|
+
processed = processed.replace(versionRegex, "");
|
|
245
|
+
processed = processed.trimStart();
|
|
249
246
|
|
|
250
|
-
// 2. Build auto-injected math constants defines
|
|
251
247
|
let constantsBlock = "";
|
|
252
248
|
if (usedConstants.size > 0) {
|
|
253
249
|
constantsBlock = "// --- GLSLH Auto-injected Math Constants ---\n";
|
|
@@ -258,7 +254,6 @@ export class GLSLHParser {
|
|
|
258
254
|
constantsBlock += "\n";
|
|
259
255
|
}
|
|
260
256
|
|
|
261
|
-
// 3. Build auto-injected uniform declarations
|
|
262
257
|
let declBlock = "";
|
|
263
258
|
const declLines = Object.entries(uniformsDefinition)
|
|
264
259
|
.filter(
|
|
@@ -273,8 +268,7 @@ export class GLSLHParser {
|
|
|
273
268
|
"\n\n";
|
|
274
269
|
}
|
|
275
270
|
|
|
276
|
-
|
|
277
|
-
processed = versionHeader + constantsBlock + declBlock + processed;
|
|
271
|
+
processed = constantsBlock + declBlock + processed;
|
|
278
272
|
|
|
279
273
|
return {
|
|
280
274
|
source: processed,
|
|
@@ -282,88 +276,3 @@ export class GLSLHParser {
|
|
|
282
276
|
};
|
|
283
277
|
}
|
|
284
278
|
}
|
|
285
|
-
|
|
286
|
-
function detectFunctionCollisions(flattenedSource) {
|
|
287
|
-
const funcRegex =
|
|
288
|
-
/\b(float|int|bool|vec2|vec3|vec4|mat2|mat3|mat4|void)\s+(\w+)\s*\(([^)]*)\)/g;
|
|
289
|
-
const registry = new Map();
|
|
290
|
-
const lines = flattenedSource.split(/\r?\n/);
|
|
291
|
-
let currentFile = "root";
|
|
292
|
-
|
|
293
|
-
for (const line of lines) {
|
|
294
|
-
const beginMatch = line.match(/\/\/\s*---\s*BEGIN INCLUDE:\s*(.+)\s*---/);
|
|
295
|
-
if (beginMatch) {
|
|
296
|
-
currentFile = beginMatch[1].trim();
|
|
297
|
-
continue;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
let match;
|
|
301
|
-
while ((match = funcRegex.exec(line)) !== null) {
|
|
302
|
-
const returnType = match[1];
|
|
303
|
-
const funcName = match[2];
|
|
304
|
-
const params = match[3].trim().replace(/\s+/g, " ");
|
|
305
|
-
|
|
306
|
-
if (funcName === "main") continue;
|
|
307
|
-
const signatureKey = `${funcName}(${params})`;
|
|
308
|
-
|
|
309
|
-
if (registry.has(signatureKey)) {
|
|
310
|
-
const firstFile = registry.get(signatureKey);
|
|
311
|
-
if (firstFile !== currentFile) {
|
|
312
|
-
console.warn(
|
|
313
|
-
`⚠️ GLSLH Duplicate Definition Warning: Exact signature '${signatureKey}' is defined in both '${firstFile}' and '${currentFile}'. This will cause a compilation clash in WebGL.`,
|
|
314
|
-
);
|
|
315
|
-
}
|
|
316
|
-
} else {
|
|
317
|
-
registry.set(signatureKey, currentFile);
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
export function translateShaderError(
|
|
324
|
-
errorMessage,
|
|
325
|
-
flattenedSource,
|
|
326
|
-
rootFilePath = "shader.glslh",
|
|
327
|
-
) {
|
|
328
|
-
const lines = flattenedSource.split(/\r?\n/);
|
|
329
|
-
const lineMap = [];
|
|
330
|
-
const fileStack = [rootFilePath];
|
|
331
|
-
const lineCounters = { [rootFilePath]: 0 };
|
|
332
|
-
|
|
333
|
-
for (const line of lines) {
|
|
334
|
-
const beginMatch = line.match(/\/\/\s*---\s*BEGIN INCLUDE:\s*(.+)\s*---/);
|
|
335
|
-
const endMatch = line.match(/\/\/\s*---\s*END INCLUDE:\s*(.+)\s*---/);
|
|
336
|
-
|
|
337
|
-
if (beginMatch) {
|
|
338
|
-
const incPath = beginMatch[1].trim();
|
|
339
|
-
fileStack.push(incPath);
|
|
340
|
-
lineCounters[incPath] = 0;
|
|
341
|
-
lineMap.push({ file: incPath, line: -1, isMarker: true });
|
|
342
|
-
continue;
|
|
343
|
-
} else if (endMatch) {
|
|
344
|
-
fileStack.pop();
|
|
345
|
-
lineMap.push({
|
|
346
|
-
file: fileStack.at(-1),
|
|
347
|
-
line: -1,
|
|
348
|
-
isMarker: true,
|
|
349
|
-
});
|
|
350
|
-
continue;
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
const currentFile = fileStack.at(-1);
|
|
354
|
-
lineCounters[currentFile] = (lineCounters[currentFile] || 0) + 1;
|
|
355
|
-
lineMap.push({ file: currentFile, line: lineCounters[currentFile] });
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
const errorRegex = /(?:ERROR:\s*0:|Line\s+|:\s*0:)(\d+)/gi;
|
|
359
|
-
|
|
360
|
-
return errorMessage.replace(errorRegex, (match, lineNumStr) => {
|
|
361
|
-
const flatLine = Number.parseInt(lineNumStr, 10);
|
|
362
|
-
const mapped = lineMap.at(flatLine - 1);
|
|
363
|
-
|
|
364
|
-
if (mapped && !mapped.isMarker) {
|
|
365
|
-
return `[${mapped.file}:${mapped.line}]`;
|
|
366
|
-
}
|
|
367
|
-
return match;
|
|
368
|
-
});
|
|
369
|
-
}
|
package/src/validator.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
export function detectFunctionCollisions(flattenedSource) {
|
|
2
|
+
const funcRegex =
|
|
3
|
+
/\b(float|int|bool|vec2|vec3|vec4|mat2|mat3|mat4|void)\s+(\w+)\s*\(([^)]*)\)\s*(;|\{[^}]*\})/g;
|
|
4
|
+
|
|
5
|
+
const funcRegistry = new Map();
|
|
6
|
+
const lines = flattenedSource.split(/\r?\n/);
|
|
7
|
+
let currentFile = "root";
|
|
8
|
+
|
|
9
|
+
for (const line of lines) {
|
|
10
|
+
const beginMatch = line.match(/\/\/\s*---\s*BEGIN INCLUDE:\s*(.+)\s*---/);
|
|
11
|
+
if (beginMatch) {
|
|
12
|
+
currentFile = beginMatch[1].trim();
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let match;
|
|
17
|
+
funcRegex.lastIndex = 0;
|
|
18
|
+
while ((match = funcRegex.exec(line)) !== null) {
|
|
19
|
+
const returnType = match[1];
|
|
20
|
+
const funcName = match[2];
|
|
21
|
+
const params = match[3].trim().replace(/\s+/g, " ");
|
|
22
|
+
const bodyOrSemi = match[4].trim();
|
|
23
|
+
|
|
24
|
+
if (funcName === "main") continue;
|
|
25
|
+
if (bodyOrSemi.startsWith(";")) continue;
|
|
26
|
+
|
|
27
|
+
if (!funcRegistry.has(funcName)) {
|
|
28
|
+
funcRegistry.set(funcName, new Map());
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const overloads = funcRegistry.get(funcName);
|
|
32
|
+
const signatureKey = `(${params})`;
|
|
33
|
+
|
|
34
|
+
if (overloads.has(signatureKey)) {
|
|
35
|
+
const firstFile = overloads.get(signatureKey);
|
|
36
|
+
if (firstFile !== currentFile) {
|
|
37
|
+
console.warn(
|
|
38
|
+
`⚠️ GLSLH Duplicate Definition Warning: Exact signature '${funcName}${signatureKey}' is defined in both '${firstFile}' and '${currentFile}'. This will cause a compilation clash in WebGL.`,
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
} else {
|
|
42
|
+
overloads.set(signatureKey, currentFile);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function translateShaderError(
|
|
49
|
+
errorMessage,
|
|
50
|
+
flattenedSource,
|
|
51
|
+
rootFilePath = "shader.glslh",
|
|
52
|
+
) {
|
|
53
|
+
const lines = flattenedSource.split(/\r?\n/);
|
|
54
|
+
const lineMap = [];
|
|
55
|
+
const fileStack = [rootFilePath];
|
|
56
|
+
const lineCounters = { [rootFilePath]: 0 };
|
|
57
|
+
|
|
58
|
+
for (const line of lines) {
|
|
59
|
+
const beginMatch = line.match(/\/\/\s*---\s*BEGIN INCLUDE:\s*(.+)\s*---/);
|
|
60
|
+
const endMatch = line.match(/\/\/\s*---\s*END INCLUDE:\s*(.+)\s*---/);
|
|
61
|
+
|
|
62
|
+
if (beginMatch) {
|
|
63
|
+
const incPath = beginMatch[1].trim();
|
|
64
|
+
fileStack.push(incPath);
|
|
65
|
+
lineCounters[incPath] = 0;
|
|
66
|
+
lineMap.push({ file: incPath, line: -1, isMarker: true });
|
|
67
|
+
continue;
|
|
68
|
+
} else if (endMatch) {
|
|
69
|
+
fileStack.pop();
|
|
70
|
+
lineMap.push({
|
|
71
|
+
file: fileStack.at(-1),
|
|
72
|
+
line: -1,
|
|
73
|
+
isMarker: true,
|
|
74
|
+
});
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const currentFile = fileStack.at(-1);
|
|
79
|
+
lineCounters[currentFile] = (lineCounters[currentFile] || 0) + 1;
|
|
80
|
+
lineMap.push({ file: currentFile, line: lineCounters[currentFile] });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const errorRegex = /(?:ERROR:\s*0:|Line\s+|:\s*0:)(\d+)/gi;
|
|
84
|
+
|
|
85
|
+
return errorMessage.replace(errorRegex, (match, lineNumStr) => {
|
|
86
|
+
const flatLine = Number.parseInt(lineNumStr, 10);
|
|
87
|
+
const mapped = lineMap.at(flatLine - 1);
|
|
88
|
+
|
|
89
|
+
if (mapped && !mapped.isMarker) {
|
|
90
|
+
const contextStart = Math.max(0, flatLine - 3);
|
|
91
|
+
const contextEnd = Math.min(lines.length - 1, flatLine + 1);
|
|
92
|
+
let snippet = "\n--- Code Context ---\n";
|
|
93
|
+
|
|
94
|
+
for (let i = contextStart; i <= contextEnd; i++) {
|
|
95
|
+
const prefix = i === flatLine - 1 ? " >> " : " ";
|
|
96
|
+
snippet += `${prefix}${i + 1}: ${lines[i]}\n`;
|
|
97
|
+
}
|
|
98
|
+
snippet += "--------------------";
|
|
99
|
+
|
|
100
|
+
return `[File: ${mapped.file} | Line: ${mapped.line}]${snippet}`;
|
|
101
|
+
}
|
|
102
|
+
return match;
|
|
103
|
+
});
|
|
104
|
+
}
|