@doccident/doccident 0.0.3 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +230 -6
- package/dist/doctest.js +40 -13
- package/dist/languages/c.js +58 -0
- package/dist/languages/cobol.js +50 -0
- package/dist/languages/fortran.js +56 -0
- package/dist/languages/go.js +45 -0
- package/dist/languages/interface.js +2 -0
- package/dist/languages/javascript.js +24 -0
- package/dist/languages/python.js +31 -0
- package/dist/languages/rust.js +56 -0
- package/dist/languages/shell.js +37 -0
- package/dist/parse-code-snippets-from-markdown.js +37 -6
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -1,17 +1,30 @@
|
|
|
1
1
|
# doccident
|
|
2
2
|
|
|
3
3
|
[](http://badge.fury.io/js/doccident)
|
|
4
|
+
[](README.md)
|
|
4
5
|
|
|
5
6
|
**Test the code examples in your Markdown documentation.**
|
|
6
7
|
|
|
7
8
|
## Overview
|
|
8
9
|
|
|
9
|
-
As an open source developer, few things are more frustrating for users than encountering broken examples in a README. `doccident` ensures your documentation remains accurate by treating your examples as testable code. It parses your Markdown files, extracts
|
|
10
|
+
As an open source developer, few things are more frustrating for users than encountering broken examples in a README. `doccident` ensures your documentation remains accurate by treating your examples as testable code. It parses your Markdown files, extracts code blocks for multiple languages, and executes them in a sandboxed environment to verify they run without errors.
|
|
11
|
+
|
|
12
|
+
**Supported Languages:**
|
|
13
|
+
* JavaScript (Node.js)
|
|
14
|
+
* TypeScript
|
|
15
|
+
* Python
|
|
16
|
+
* Shell (Bash, Sh, Zsh)
|
|
17
|
+
* Go
|
|
18
|
+
* Rust
|
|
19
|
+
* Fortran
|
|
20
|
+
* COBOL
|
|
21
|
+
* C
|
|
10
22
|
|
|
11
23
|
> **Note**: `doccident` primarily verifies that your code *runs* without throwing exceptions. While you can add assertions to your examples to test correctness, its main goal is to ensure your documentation examples are valid and runnable.
|
|
12
24
|
|
|
13
25
|
## Installation
|
|
14
26
|
|
|
27
|
+
<!-- skip-example -->
|
|
15
28
|
```bash
|
|
16
29
|
npm install --save-dev @doccident/doccident
|
|
17
30
|
```
|
|
@@ -20,24 +33,41 @@ npm install --save-dev @doccident/doccident
|
|
|
20
33
|
|
|
21
34
|
Run `doccident` in your project root. By default, it recursively checks all `.md` and `.markdown` files (excluding `node_modules`).
|
|
22
35
|
|
|
36
|
+
<!-- skip-example -->
|
|
23
37
|
```bash
|
|
24
38
|
npx doccident
|
|
25
39
|
```
|
|
26
40
|
|
|
27
41
|
You can also target specific files or directories:
|
|
28
42
|
|
|
43
|
+
<!-- skip-example -->
|
|
29
44
|
```bash
|
|
30
45
|
npx doccident docs/**/*.md
|
|
31
46
|
```
|
|
32
47
|
|
|
33
48
|
### Language Support & Recipes
|
|
34
49
|
|
|
35
|
-
`doccident` executes code inside
|
|
50
|
+
`doccident` executes code inside fenced code blocks for the following languages:
|
|
51
|
+
|
|
52
|
+
* **JavaScript**: `js`, `javascript`, `es6`
|
|
53
|
+
* **TypeScript**: `ts`, `typescript`
|
|
54
|
+
* **Python**: `py`, `python`
|
|
55
|
+
* **Shell**: `sh`, `bash`, `zsh`, `shell`
|
|
56
|
+
* **Go**: `go`
|
|
57
|
+
* **Rust**: `rust`, `rs`
|
|
58
|
+
* **Fortran**: `fortran`, `f90`, `f95`
|
|
59
|
+
* **COBOL**: `cobol`, `cob`
|
|
60
|
+
* **C**: `c`
|
|
61
|
+
|
|
62
|
+
It automatically transforms modern JavaScript and TypeScript using **esbuild** before execution.
|
|
36
63
|
|
|
37
64
|
#### JavaScript
|
|
38
65
|
|
|
39
66
|
Use `js`, `javascript`, or `es6` for JavaScript examples.
|
|
40
67
|
|
|
68
|
+
**Execution Model:**
|
|
69
|
+
Runs directly in a Node.js `vm` sandbox.
|
|
70
|
+
|
|
41
71
|
**Recipe:**
|
|
42
72
|
|
|
43
73
|
1. Use `js` fenced code blocks.
|
|
@@ -53,6 +83,9 @@ Use `js`, `javascript`, or `es6` for JavaScript examples.
|
|
|
53
83
|
|
|
54
84
|
Use `ts` or `typescript` for TypeScript examples.
|
|
55
85
|
|
|
86
|
+
**Execution Model:**
|
|
87
|
+
Transpiled via `esbuild`, then runs in a Node.js `vm` sandbox.
|
|
88
|
+
|
|
56
89
|
**Recipe:**
|
|
57
90
|
|
|
58
91
|
1. Use `ts` fenced code blocks.
|
|
@@ -69,6 +102,179 @@ Use `ts` or `typescript` for TypeScript examples.
|
|
|
69
102
|
console.log(user.name);
|
|
70
103
|
```
|
|
71
104
|
|
|
105
|
+
#### Python
|
|
106
|
+
|
|
107
|
+
Use `py` or `python` for Python examples.
|
|
108
|
+
|
|
109
|
+
**Prerequisites:**
|
|
110
|
+
* `python3` must be installed and available in your system path.
|
|
111
|
+
* **macOS**: `brew install python`
|
|
112
|
+
* **Ubuntu/Debian**: `sudo apt-get install python3`
|
|
113
|
+
|
|
114
|
+
**Execution Model:**
|
|
115
|
+
Spawns a `python3` subprocess with the code piped to stdin.
|
|
116
|
+
|
|
117
|
+
**Recipe:**
|
|
118
|
+
|
|
119
|
+
1. Use `python` fenced code blocks.
|
|
120
|
+
2. Standard library imports work out of the box.
|
|
121
|
+
3. State can be shared between blocks using `<!-- share-code-between-examples -->`.
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
import json
|
|
125
|
+
data = {"key": "value"}
|
|
126
|
+
assert json.loads('{"key": "value"}') == data
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
#### Shell Scripts
|
|
130
|
+
|
|
131
|
+
Use `sh`, `bash`, `zsh` or `shell` (defaults to bash) for shell examples.
|
|
132
|
+
|
|
133
|
+
**Prerequisites:**
|
|
134
|
+
* The specified shell (`bash`, `sh`, or `zsh`) must be available in your system path.
|
|
135
|
+
|
|
136
|
+
**Execution Model:**
|
|
137
|
+
Spawns a subprocess using the specified shell, with the code piped to stdin.
|
|
138
|
+
|
|
139
|
+
**Recipe:**
|
|
140
|
+
|
|
141
|
+
1. Use specific shell tags like `bash` or `zsh` if your script relies on shell-specific syntax.
|
|
142
|
+
2. Use `sh` for POSIX-compliant scripts.
|
|
143
|
+
3. State (variables) can be shared between blocks using `<!-- share-code-between-examples -->`.
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
export MY_VAR="hello"
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
if [ "$MY_VAR" != "hello" ]; then exit 1; fi
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
#### Go
|
|
154
|
+
|
|
155
|
+
Use `go` for Go examples.
|
|
156
|
+
|
|
157
|
+
**Prerequisites:**
|
|
158
|
+
* `go` must be installed and available in your system path.
|
|
159
|
+
* **macOS**: `brew install go`
|
|
160
|
+
* **Ubuntu/Debian**: `sudo apt-get install golang-go`
|
|
161
|
+
|
|
162
|
+
**Execution Model:**
|
|
163
|
+
Writes code to a temporary file and executes it via `go run`.
|
|
164
|
+
|
|
165
|
+
**Recipe:**
|
|
166
|
+
|
|
167
|
+
1. Use `go` fenced code blocks.
|
|
168
|
+
2. You can provide a full program (including `package main`) OR a simple snippet.
|
|
169
|
+
3. Simple snippets (without `package main`) are automatically wrapped in a `main` function and include `import "fmt"`.
|
|
170
|
+
4. **Note**: `<!-- share-code-between-examples -->` is **not** supported for Go.
|
|
171
|
+
|
|
172
|
+
**Simple Snippet:**
|
|
173
|
+
```go
|
|
174
|
+
fmt.Println("Hello Go")
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
#### Rust
|
|
178
|
+
|
|
179
|
+
Use `rust` or `rs` for Rust examples.
|
|
180
|
+
|
|
181
|
+
**Prerequisites:**
|
|
182
|
+
* `rustc` (Rust compiler) must be installed and available in your system path.
|
|
183
|
+
* **macOS**: `brew install rust`
|
|
184
|
+
* **Ubuntu/Debian**: `curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh` or `sudo apt-get install rustc`
|
|
185
|
+
|
|
186
|
+
**Execution Model:**
|
|
187
|
+
Compiles the code using `rustc` into a temporary binary, then executes the binary.
|
|
188
|
+
|
|
189
|
+
**Recipe:**
|
|
190
|
+
|
|
191
|
+
1. Use `rust` fenced code blocks.
|
|
192
|
+
2. You can provide a full program (including `fn main()`) OR a simple snippet.
|
|
193
|
+
3. Simple snippets (without `fn main`) are automatically wrapped in a `main` function.
|
|
194
|
+
4. **Note**: `<!-- share-code-between-examples -->` is **not** supported for Rust.
|
|
195
|
+
|
|
196
|
+
**Simple Snippet:**
|
|
197
|
+
```rust
|
|
198
|
+
println!("Hello Rust");
|
|
199
|
+
let x = 5;
|
|
200
|
+
assert_eq!(x, 5);
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
#### Fortran
|
|
204
|
+
|
|
205
|
+
Use `fortran`, `f90`, or `f95` for Fortran examples.
|
|
206
|
+
|
|
207
|
+
**Prerequisites:**
|
|
208
|
+
* `gfortran` must be installed and available in your system path.
|
|
209
|
+
* **macOS**: `brew install gcc` (includes gfortran)
|
|
210
|
+
* **Ubuntu/Debian**: `sudo apt-get install gfortran`
|
|
211
|
+
|
|
212
|
+
**Execution Model:**
|
|
213
|
+
Compiles the code using `gfortran` into a temporary binary, then executes the binary.
|
|
214
|
+
|
|
215
|
+
**Recipe:**
|
|
216
|
+
|
|
217
|
+
1. Use `fortran` fenced code blocks.
|
|
218
|
+
2. You can provide a full program (starting with `program name`) OR a simple snippet.
|
|
219
|
+
3. Simple snippets are automatically wrapped in a `program main ... end program main` block.
|
|
220
|
+
4. **Note**: `<!-- share-code-between-examples -->` is **not** supported for Fortran.
|
|
221
|
+
|
|
222
|
+
**Simple Snippet:**
|
|
223
|
+
```fortran
|
|
224
|
+
print *, "Hello Fortran"
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
#### COBOL
|
|
228
|
+
|
|
229
|
+
Use `cobol` or `cob` for COBOL examples.
|
|
230
|
+
|
|
231
|
+
**Prerequisites:**
|
|
232
|
+
* `cobc` (GnuCOBOL) must be installed and available in your system path.
|
|
233
|
+
* **macOS**: `brew install gnucobol`
|
|
234
|
+
* **Ubuntu/Debian**: `sudo apt-get install gnucobol`
|
|
235
|
+
|
|
236
|
+
**Execution Model:**
|
|
237
|
+
Compiles the code using `cobc -x -free` into a temporary executable, then runs it.
|
|
238
|
+
|
|
239
|
+
**Recipe:**
|
|
240
|
+
|
|
241
|
+
1. Use `cobol` fenced code blocks.
|
|
242
|
+
2. Provide a full COBOL program (including `IDENTIFICATION DIVISION`).
|
|
243
|
+
3. The compiler is run in free-format mode (`-free`).
|
|
244
|
+
4. **Note**: `<!-- share-code-between-examples -->` is **not** supported for COBOL.
|
|
245
|
+
|
|
246
|
+
```cobol
|
|
247
|
+
IDENTIFICATION DIVISION.
|
|
248
|
+
PROGRAM-ID. HELLO.
|
|
249
|
+
PROCEDURE DIVISION.
|
|
250
|
+
DISPLAY 'Hello COBOL'.
|
|
251
|
+
STOP RUN.
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
#### C
|
|
255
|
+
|
|
256
|
+
Use `c` for C examples.
|
|
257
|
+
|
|
258
|
+
**Prerequisites:**
|
|
259
|
+
* `gcc` must be installed and available in your system path.
|
|
260
|
+
* **macOS**: `xcode-select --install` or `brew install gcc`
|
|
261
|
+
* **Ubuntu/Debian**: `sudo apt-get install build-essential`
|
|
262
|
+
|
|
263
|
+
**Execution Model:**
|
|
264
|
+
Compiles the code using `gcc` into a temporary binary, then executes the binary.
|
|
265
|
+
|
|
266
|
+
**Recipe:**
|
|
267
|
+
|
|
268
|
+
1. Use `c` fenced code blocks.
|
|
269
|
+
2. You can provide a full program (including `main()`) OR a simple snippet.
|
|
270
|
+
3. Simple snippets are automatically wrapped in a `main` function and include `stdio.h`.
|
|
271
|
+
4. **Note**: `<!-- share-code-between-examples -->` is **not** supported for C.
|
|
272
|
+
|
|
273
|
+
**Simple Snippet:**
|
|
274
|
+
```c
|
|
275
|
+
printf("Hello C\n");
|
|
276
|
+
```
|
|
277
|
+
|
|
72
278
|
### Skipping Examples
|
|
73
279
|
|
|
74
280
|
To skip a specific code block, add the `<!-- skip-example -->` comment immediately before it:
|
|
@@ -79,6 +285,23 @@ To skip a specific code block, add the `<!-- skip-example -->` comment immediate
|
|
|
79
285
|
fetch('https://example.com');
|
|
80
286
|
```
|
|
81
287
|
|
|
288
|
+
### Sharing Code Between Examples
|
|
289
|
+
|
|
290
|
+
By default, each code block is executed in isolation. To share state (variables, functions, classes) between multiple code blocks in the same file, add the `<!-- share-code-between-examples -->` comment. This applies to the entire file.
|
|
291
|
+
|
|
292
|
+
> **Note**: This feature is currently supported for JavaScript, TypeScript, Python, and Shell, but **not** compiled languages like Go, Rust, Fortran, COBOL, and C.
|
|
293
|
+
|
|
294
|
+
<!-- share-code-between-examples -->
|
|
295
|
+
|
|
296
|
+
```python
|
|
297
|
+
x = 10
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
```python
|
|
301
|
+
# x is still available here
|
|
302
|
+
assert x == 10
|
|
303
|
+
```
|
|
304
|
+
|
|
82
305
|
## Configuration
|
|
83
306
|
|
|
84
307
|
Create a `.doccident-setup.js` file in your project root to configure the test environment.
|
|
@@ -94,7 +317,7 @@ module.exports = {
|
|
|
94
317
|
// Make 'my-library' available when examples call require('my-library')
|
|
95
318
|
'my-library': require('./index.js'),
|
|
96
319
|
'lodash': require('lodash')
|
|
97
|
-
|
|
320
|
+
}
|
|
98
321
|
};
|
|
99
322
|
```
|
|
100
323
|
|
|
@@ -127,13 +350,14 @@ module.exports = {
|
|
|
127
350
|
* Reads Markdown content line-by-line.
|
|
128
351
|
* Uses a robust state machine to identify code fences and control comments (like `skip-example`).
|
|
129
352
|
* Extracts valid snippets into structured objects containing code, file paths, and line numbers.
|
|
130
|
-
* **
|
|
353
|
+
* **Multi-Language Support**: Recognizes `js`, `ts`, `python`, `shell`, `go`, `rust`, `fortran`, `cobol`, and `c` blocks.
|
|
131
354
|
|
|
132
355
|
2. **Test Runner (`src/doctest.ts`)**
|
|
133
356
|
* The orchestrator of the application.
|
|
134
357
|
* Iterates through parsed snippets and manages the execution lifecycle.
|
|
135
|
-
* **Sandboxing**: Uses Node.js's `vm` module (`runInNewContext`) to execute code in isolation.
|
|
136
|
-
* **
|
|
358
|
+
* **Sandboxing**: Uses Node.js's `vm` module (`runInNewContext`) to execute JS/TS code in isolation.
|
|
359
|
+
* **Subprocess Execution**: Spawns `python3`, `go`, `rustc`, `gfortran`, `cobc`, `gcc`, or shell subprocesses for non-JS languages.
|
|
360
|
+
* **Transformation**: Uses **esbuild** to compile modern JavaScript and TypeScript code down to a compatible format before execution.
|
|
137
361
|
|
|
138
362
|
3. **Reporter (`src/reporter.ts`)**
|
|
139
363
|
* Collects execution results (pass, fail, skip).
|
package/dist/doctest.js
CHANGED
|
@@ -6,13 +6,19 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.printResults = void 0;
|
|
7
7
|
exports.runTests = runTests;
|
|
8
8
|
const fs_1 = require("fs");
|
|
9
|
-
const vm_1 = require("vm");
|
|
10
|
-
const esbuild_1 = require("esbuild");
|
|
11
9
|
const chalk_1 = __importDefault(require("chalk"));
|
|
12
10
|
const utils_1 = require("./utils");
|
|
13
11
|
const parse_code_snippets_from_markdown_1 = __importDefault(require("./parse-code-snippets-from-markdown"));
|
|
14
12
|
const reporter_1 = require("./reporter");
|
|
15
13
|
Object.defineProperty(exports, "printResults", { enumerable: true, get: function () { return reporter_1.printResults; } });
|
|
14
|
+
const python_1 = require("./languages/python");
|
|
15
|
+
const shell_1 = require("./languages/shell");
|
|
16
|
+
const go_1 = require("./languages/go");
|
|
17
|
+
const rust_1 = require("./languages/rust");
|
|
18
|
+
const fortran_1 = require("./languages/fortran");
|
|
19
|
+
const cobol_1 = require("./languages/cobol");
|
|
20
|
+
const c_1 = require("./languages/c");
|
|
21
|
+
const javascript_1 = require("./languages/javascript");
|
|
16
22
|
function runTests(files, config) {
|
|
17
23
|
const results = files
|
|
18
24
|
.map(read)
|
|
@@ -78,25 +84,46 @@ function test(config, _filename, sandbox) {
|
|
|
78
84
|
}
|
|
79
85
|
}
|
|
80
86
|
let perSnippetSandbox;
|
|
87
|
+
let activeSandbox;
|
|
88
|
+
let isSharedSandbox = false;
|
|
81
89
|
if (sandbox === undefined) {
|
|
82
90
|
perSnippetSandbox = makeTestSandbox(config);
|
|
91
|
+
activeSandbox = perSnippetSandbox;
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
activeSandbox = sandbox;
|
|
95
|
+
isSharedSandbox = true;
|
|
83
96
|
}
|
|
84
97
|
if (config.beforeEach) {
|
|
85
98
|
config.beforeEach();
|
|
86
99
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
100
|
+
let result;
|
|
101
|
+
if (codeSnippet.language === 'python') {
|
|
102
|
+
result = (0, python_1.pythonHandler)(code, codeSnippet, config, activeSandbox, isSharedSandbox);
|
|
103
|
+
}
|
|
104
|
+
else if (['bash', 'sh', 'zsh'].includes(codeSnippet.language || '')) {
|
|
105
|
+
result = (0, shell_1.shellHandler)(code, codeSnippet, config, activeSandbox, isSharedSandbox);
|
|
106
|
+
}
|
|
107
|
+
else if (codeSnippet.language === 'go') {
|
|
108
|
+
result = (0, go_1.goHandler)(code, codeSnippet, config, activeSandbox, isSharedSandbox);
|
|
109
|
+
}
|
|
110
|
+
else if (codeSnippet.language === 'rust') {
|
|
111
|
+
result = (0, rust_1.rustHandler)(code, codeSnippet, config, activeSandbox, isSharedSandbox);
|
|
96
112
|
}
|
|
97
|
-
|
|
98
|
-
|
|
113
|
+
else if (codeSnippet.language === 'fortran') {
|
|
114
|
+
result = (0, fortran_1.fortranHandler)(code, codeSnippet, config, activeSandbox, isSharedSandbox);
|
|
115
|
+
}
|
|
116
|
+
else if (codeSnippet.language === 'cobol') {
|
|
117
|
+
result = (0, cobol_1.cobolHandler)(code, codeSnippet, config, activeSandbox, isSharedSandbox);
|
|
118
|
+
}
|
|
119
|
+
else if (codeSnippet.language === 'c') {
|
|
120
|
+
result = (0, c_1.cHandler)(code, codeSnippet, config, activeSandbox, isSharedSandbox);
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
result = (0, javascript_1.javascriptHandler)(code, codeSnippet, config, activeSandbox, isSharedSandbox);
|
|
99
124
|
}
|
|
125
|
+
success = result.success;
|
|
126
|
+
stack = result.stack;
|
|
100
127
|
const status = success ? "pass" : "fail";
|
|
101
128
|
process.stdout.write(success ? chalk_1.default.green(".") : chalk_1.default.red("x"));
|
|
102
129
|
return { status, codeSnippet, stack };
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.cHandler = void 0;
|
|
4
|
+
const child_process_1 = require("child_process");
|
|
5
|
+
const fs_1 = require("fs");
|
|
6
|
+
const path_1 = require("path");
|
|
7
|
+
const os_1 = require("os");
|
|
8
|
+
const cHandler = (code, _snippet, _config, _sandbox, _isSharedSandbox) => {
|
|
9
|
+
let success = false;
|
|
10
|
+
let stack = "";
|
|
11
|
+
// C execution logic
|
|
12
|
+
// Auto-wrap in main function if not present
|
|
13
|
+
let cCode = code;
|
|
14
|
+
if (!cCode.includes('main(')) {
|
|
15
|
+
cCode = `#include <stdio.h>
|
|
16
|
+
int main() {
|
|
17
|
+
${cCode}
|
|
18
|
+
return 0;
|
|
19
|
+
}`;
|
|
20
|
+
}
|
|
21
|
+
const uniqueId = `${Date.now()}_${Math.random().toString(36).substring(7)}`;
|
|
22
|
+
const tempSourceFile = (0, path_1.join)((0, os_1.tmpdir)(), `doccident_c_${uniqueId}.c`);
|
|
23
|
+
const tempExeFile = (0, path_1.join)((0, os_1.tmpdir)(), `doccident_c_${uniqueId}`);
|
|
24
|
+
try {
|
|
25
|
+
(0, fs_1.writeFileSync)(tempSourceFile, cCode);
|
|
26
|
+
// Compile with gcc
|
|
27
|
+
const compileResult = (0, child_process_1.spawnSync)('gcc', [tempSourceFile, '-o', tempExeFile], { encoding: 'utf-8' });
|
|
28
|
+
if (compileResult.status !== 0) {
|
|
29
|
+
stack = compileResult.stderr || "C compilation failed";
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
// Run
|
|
33
|
+
const runResult = (0, child_process_1.spawnSync)(tempExeFile, [], { encoding: 'utf-8' });
|
|
34
|
+
if (runResult.status === 0) {
|
|
35
|
+
success = true;
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
stack = runResult.stderr || "C execution failed with non-zero exit code";
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
catch (e) {
|
|
43
|
+
stack = e.message || "Failed to execute gcc or run binary";
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
try {
|
|
47
|
+
if ((0, fs_1.existsSync)(tempSourceFile))
|
|
48
|
+
(0, fs_1.unlinkSync)(tempSourceFile);
|
|
49
|
+
if ((0, fs_1.existsSync)(tempExeFile))
|
|
50
|
+
(0, fs_1.unlinkSync)(tempExeFile);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// Ignore cleanup error
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return { success, stack };
|
|
57
|
+
};
|
|
58
|
+
exports.cHandler = cHandler;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.cobolHandler = void 0;
|
|
4
|
+
const child_process_1 = require("child_process");
|
|
5
|
+
const fs_1 = require("fs");
|
|
6
|
+
const path_1 = require("path");
|
|
7
|
+
const os_1 = require("os");
|
|
8
|
+
const cobolHandler = (code, _snippet, _config, _sandbox, _isSharedSandbox) => {
|
|
9
|
+
let success = false;
|
|
10
|
+
let stack = "";
|
|
11
|
+
// COBOL execution logic
|
|
12
|
+
// Use shorter filename as cobc has strict length limits
|
|
13
|
+
const uniqueId = Math.random().toString(36).substring(2, 10);
|
|
14
|
+
const tempSourceFile = (0, path_1.join)((0, os_1.tmpdir)(), `cob_${uniqueId}.cob`);
|
|
15
|
+
const tempExeFile = (0, path_1.join)((0, os_1.tmpdir)(), `cob_${uniqueId}`);
|
|
16
|
+
try {
|
|
17
|
+
(0, fs_1.writeFileSync)(tempSourceFile, code);
|
|
18
|
+
// Compile with cobc -x (executable) -free (free format)
|
|
19
|
+
const compileResult = (0, child_process_1.spawnSync)('cobc', ['-x', '-free', '-o', tempExeFile, tempSourceFile], { encoding: 'utf-8' });
|
|
20
|
+
if (compileResult.status !== 0) {
|
|
21
|
+
stack = compileResult.stderr || "COBOL compilation failed";
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
// Run
|
|
25
|
+
const runResult = (0, child_process_1.spawnSync)(tempExeFile, [], { encoding: 'utf-8' });
|
|
26
|
+
if (runResult.status === 0) {
|
|
27
|
+
success = true;
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
stack = runResult.stderr || "COBOL execution failed with non-zero exit code";
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
catch (e) {
|
|
35
|
+
stack = e.message || "Failed to execute cobc or run binary";
|
|
36
|
+
}
|
|
37
|
+
finally {
|
|
38
|
+
try {
|
|
39
|
+
if ((0, fs_1.existsSync)(tempSourceFile))
|
|
40
|
+
(0, fs_1.unlinkSync)(tempSourceFile);
|
|
41
|
+
if ((0, fs_1.existsSync)(tempExeFile))
|
|
42
|
+
(0, fs_1.unlinkSync)(tempExeFile);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// Ignore cleanup error
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return { success, stack };
|
|
49
|
+
};
|
|
50
|
+
exports.cobolHandler = cobolHandler;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.fortranHandler = void 0;
|
|
4
|
+
const child_process_1 = require("child_process");
|
|
5
|
+
const fs_1 = require("fs");
|
|
6
|
+
const path_1 = require("path");
|
|
7
|
+
const os_1 = require("os");
|
|
8
|
+
const fortranHandler = (code, _snippet, _config, _sandbox, _isSharedSandbox) => {
|
|
9
|
+
let success = false;
|
|
10
|
+
let stack = "";
|
|
11
|
+
// Fortran execution logic
|
|
12
|
+
// Auto-wrap if 'program' is missing
|
|
13
|
+
let fortranCode = code;
|
|
14
|
+
if (!fortranCode.toLowerCase().includes('program ')) {
|
|
15
|
+
fortranCode = `program main
|
|
16
|
+
${fortranCode}
|
|
17
|
+
end program main`;
|
|
18
|
+
}
|
|
19
|
+
const uniqueId = `${Date.now()}_${Math.random().toString(36).substring(7)}`;
|
|
20
|
+
const tempSourceFile = (0, path_1.join)((0, os_1.tmpdir)(), `doccident_fortran_${uniqueId}.f90`);
|
|
21
|
+
const tempExeFile = (0, path_1.join)((0, os_1.tmpdir)(), `doccident_fortran_${uniqueId}`);
|
|
22
|
+
try {
|
|
23
|
+
(0, fs_1.writeFileSync)(tempSourceFile, fortranCode);
|
|
24
|
+
// Compile with gfortran
|
|
25
|
+
const compileResult = (0, child_process_1.spawnSync)('gfortran', [tempSourceFile, '-o', tempExeFile], { encoding: 'utf-8' });
|
|
26
|
+
if (compileResult.status !== 0) {
|
|
27
|
+
stack = compileResult.stderr || "Fortran compilation failed";
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
// Run
|
|
31
|
+
const runResult = (0, child_process_1.spawnSync)(tempExeFile, [], { encoding: 'utf-8' });
|
|
32
|
+
if (runResult.status === 0) {
|
|
33
|
+
success = true;
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
stack = runResult.stderr || "Fortran execution failed with non-zero exit code";
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
stack = e.message || "Failed to execute gfortran or run binary";
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
try {
|
|
45
|
+
if ((0, fs_1.existsSync)(tempSourceFile))
|
|
46
|
+
(0, fs_1.unlinkSync)(tempSourceFile);
|
|
47
|
+
if ((0, fs_1.existsSync)(tempExeFile))
|
|
48
|
+
(0, fs_1.unlinkSync)(tempExeFile);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// Ignore cleanup error
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return { success, stack };
|
|
55
|
+
};
|
|
56
|
+
exports.fortranHandler = fortranHandler;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.goHandler = void 0;
|
|
4
|
+
const child_process_1 = require("child_process");
|
|
5
|
+
const fs_1 = require("fs");
|
|
6
|
+
const path_1 = require("path");
|
|
7
|
+
const os_1 = require("os");
|
|
8
|
+
const goHandler = (code, _snippet, _config, _sandbox, _isSharedSandbox) => {
|
|
9
|
+
let success = false;
|
|
10
|
+
let stack = "";
|
|
11
|
+
// No shared context for Go yet, as it's compiled and strict structure
|
|
12
|
+
// We support standalone files or body snippets wrapped in main
|
|
13
|
+
let goCode = code;
|
|
14
|
+
if (!goCode.includes('package main')) {
|
|
15
|
+
goCode = `package main
|
|
16
|
+
import "fmt"
|
|
17
|
+
func main() {
|
|
18
|
+
${goCode}
|
|
19
|
+
}`;
|
|
20
|
+
}
|
|
21
|
+
const tempFile = (0, path_1.join)((0, os_1.tmpdir)(), `doccident_go_${Date.now()}_${Math.random().toString(36).substring(7)}.go`);
|
|
22
|
+
try {
|
|
23
|
+
(0, fs_1.writeFileSync)(tempFile, goCode);
|
|
24
|
+
const result = (0, child_process_1.spawnSync)('go', ['run', tempFile], { encoding: 'utf-8' });
|
|
25
|
+
if (result.status === 0) {
|
|
26
|
+
success = true;
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
stack = result.stderr || "Go execution failed with non-zero exit code";
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
catch (e) {
|
|
33
|
+
stack = e.message || "Failed to execute go run";
|
|
34
|
+
}
|
|
35
|
+
finally {
|
|
36
|
+
try {
|
|
37
|
+
(0, fs_1.unlinkSync)(tempFile);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
// Ignore cleanup error
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return { success, stack };
|
|
44
|
+
};
|
|
45
|
+
exports.goHandler = goHandler;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.javascriptHandler = void 0;
|
|
4
|
+
const vm_1 = require("vm");
|
|
5
|
+
const esbuild_1 = require("esbuild");
|
|
6
|
+
const javascriptHandler = (code, _snippet, _config, sandbox, _isSharedSandbox) => {
|
|
7
|
+
let success = false;
|
|
8
|
+
let stack = "";
|
|
9
|
+
try {
|
|
10
|
+
const result = (0, esbuild_1.transformSync)(code, {
|
|
11
|
+
loader: 'ts',
|
|
12
|
+
format: 'cjs',
|
|
13
|
+
target: 'node12'
|
|
14
|
+
});
|
|
15
|
+
const compiledCode = result.code || "";
|
|
16
|
+
(0, vm_1.runInNewContext)(compiledCode, sandbox);
|
|
17
|
+
success = true;
|
|
18
|
+
}
|
|
19
|
+
catch (e) {
|
|
20
|
+
stack = e.stack || "";
|
|
21
|
+
}
|
|
22
|
+
return { success, stack };
|
|
23
|
+
};
|
|
24
|
+
exports.javascriptHandler = javascriptHandler;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.pythonHandler = void 0;
|
|
4
|
+
const child_process_1 = require("child_process");
|
|
5
|
+
const pythonHandler = (code, _snippet, _config, sandbox, isSharedSandbox) => {
|
|
6
|
+
let success = false;
|
|
7
|
+
let stack = "";
|
|
8
|
+
const context = sandbox;
|
|
9
|
+
// If sharing code, we need to accumulate previous python snippets
|
|
10
|
+
if (isSharedSandbox) {
|
|
11
|
+
if (!context._pythonContext) {
|
|
12
|
+
context._pythonContext = "";
|
|
13
|
+
}
|
|
14
|
+
context._pythonContext += code + "\n";
|
|
15
|
+
code = context._pythonContext;
|
|
16
|
+
}
|
|
17
|
+
try {
|
|
18
|
+
const result = (0, child_process_1.spawnSync)('python3', [], { input: code, encoding: 'utf-8' });
|
|
19
|
+
if (result.status === 0) {
|
|
20
|
+
success = true;
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
stack = result.stderr || "Python execution failed with non-zero exit code";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
catch (e) {
|
|
27
|
+
stack = e.message || "Failed to spawn python3";
|
|
28
|
+
}
|
|
29
|
+
return { success, stack };
|
|
30
|
+
};
|
|
31
|
+
exports.pythonHandler = pythonHandler;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.rustHandler = void 0;
|
|
4
|
+
const child_process_1 = require("child_process");
|
|
5
|
+
const fs_1 = require("fs");
|
|
6
|
+
const path_1 = require("path");
|
|
7
|
+
const os_1 = require("os");
|
|
8
|
+
const rustHandler = (code, _snippet, _config, _sandbox, _isSharedSandbox) => {
|
|
9
|
+
let success = false;
|
|
10
|
+
let stack = "";
|
|
11
|
+
// Rust execution logic
|
|
12
|
+
// Auto-wrap in main function if not present
|
|
13
|
+
let rustCode = code;
|
|
14
|
+
if (!rustCode.includes('fn main()')) {
|
|
15
|
+
rustCode = `fn main() {
|
|
16
|
+
${rustCode}
|
|
17
|
+
}`;
|
|
18
|
+
}
|
|
19
|
+
const uniqueId = `${Date.now()}_${Math.random().toString(36).substring(7)}`;
|
|
20
|
+
const tempSourceFile = (0, path_1.join)((0, os_1.tmpdir)(), `doccident_rust_${uniqueId}.rs`);
|
|
21
|
+
const tempExeFile = (0, path_1.join)((0, os_1.tmpdir)(), `doccident_rust_${uniqueId}`);
|
|
22
|
+
try {
|
|
23
|
+
(0, fs_1.writeFileSync)(tempSourceFile, rustCode);
|
|
24
|
+
// Compile
|
|
25
|
+
const compileResult = (0, child_process_1.spawnSync)('rustc', [tempSourceFile, '-o', tempExeFile], { encoding: 'utf-8' });
|
|
26
|
+
if (compileResult.status !== 0) {
|
|
27
|
+
stack = compileResult.stderr || "Rust compilation failed";
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
// Run
|
|
31
|
+
const runResult = (0, child_process_1.spawnSync)(tempExeFile, [], { encoding: 'utf-8' });
|
|
32
|
+
if (runResult.status === 0) {
|
|
33
|
+
success = true;
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
stack = runResult.stderr || "Rust execution failed with non-zero exit code";
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
stack = e.message || "Failed to execute rustc or run binary";
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
try {
|
|
45
|
+
if ((0, fs_1.existsSync)(tempSourceFile))
|
|
46
|
+
(0, fs_1.unlinkSync)(tempSourceFile);
|
|
47
|
+
if ((0, fs_1.existsSync)(tempExeFile))
|
|
48
|
+
(0, fs_1.unlinkSync)(tempExeFile);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// Ignore cleanup error
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return { success, stack };
|
|
55
|
+
};
|
|
56
|
+
exports.rustHandler = rustHandler;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.shellHandler = void 0;
|
|
4
|
+
const child_process_1 = require("child_process");
|
|
5
|
+
const shellHandler = (code, snippet, _config, sandbox, isSharedSandbox) => {
|
|
6
|
+
let success = false;
|
|
7
|
+
let stack = "";
|
|
8
|
+
const context = sandbox;
|
|
9
|
+
const shell = snippet.language || 'bash';
|
|
10
|
+
// If sharing code, we need to accumulate previous shell snippets
|
|
11
|
+
if (isSharedSandbox) {
|
|
12
|
+
if (!context._shellContext) {
|
|
13
|
+
context._shellContext = {};
|
|
14
|
+
}
|
|
15
|
+
if (!context._shellContext[shell]) {
|
|
16
|
+
context._shellContext[shell] = "";
|
|
17
|
+
}
|
|
18
|
+
context._shellContext[shell] += code + "\n";
|
|
19
|
+
code = context._shellContext[shell];
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
// Use the detected shell
|
|
23
|
+
const result = (0, child_process_1.spawnSync)(shell, ['-s'], { input: code, encoding: 'utf-8' });
|
|
24
|
+
if (result.status === 0) {
|
|
25
|
+
success = true;
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
const exitCode = result.status !== null ? result.status : 'signal';
|
|
29
|
+
stack = result.stderr || result.stdout || `${shell} execution failed with non-zero exit code: ${exitCode}`;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
catch (e) {
|
|
33
|
+
stack = e.message || `Failed to spawn ${shell}`;
|
|
34
|
+
}
|
|
35
|
+
return { success, stack };
|
|
36
|
+
};
|
|
37
|
+
exports.shellHandler = shellHandler;
|
|
@@ -1,15 +1,40 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
|
|
3
|
+
// Capture indentation (group 1) and language (group 2)
|
|
4
|
+
const START_REGEX = /^(\s*)```\W*(JavaScript|js|es6|ts|typescript|python|py|bash|sh|zsh|shell|go|rust|rs|fortran|f90|f95|cobol|cob|c)\s?$/i;
|
|
5
|
+
const isStartOfSnippet = (line) => line.match(START_REGEX);
|
|
4
6
|
const isEndOfSnippet = (line) => line.trim() === "```";
|
|
5
7
|
const isSkip = (line) => line.trim() === "<!-- skip-example -->";
|
|
6
8
|
const isCodeSharedInFile = (line) => line.trim() === "<!-- share-code-between-examples -->";
|
|
7
|
-
function startNewSnippet(snippets, fileName, lineNumber) {
|
|
9
|
+
function startNewSnippet(snippets, fileName, lineNumber, language, indentation) {
|
|
8
10
|
const skip = snippets.skip;
|
|
9
11
|
snippets.skip = false;
|
|
12
|
+
let normalizedLang = 'javascript';
|
|
13
|
+
const langLower = language.toLowerCase();
|
|
14
|
+
if (['python', 'py'].includes(langLower)) {
|
|
15
|
+
normalizedLang = 'python';
|
|
16
|
+
}
|
|
17
|
+
else if (['bash', 'sh', 'zsh', 'shell'].includes(langLower)) {
|
|
18
|
+
normalizedLang = langLower === 'shell' ? 'bash' : langLower;
|
|
19
|
+
}
|
|
20
|
+
else if (langLower === 'go') {
|
|
21
|
+
normalizedLang = 'go';
|
|
22
|
+
}
|
|
23
|
+
else if (['rust', 'rs'].includes(langLower)) {
|
|
24
|
+
normalizedLang = 'rust';
|
|
25
|
+
}
|
|
26
|
+
else if (['fortran', 'f90', 'f95'].includes(langLower)) {
|
|
27
|
+
normalizedLang = 'fortran';
|
|
28
|
+
}
|
|
29
|
+
else if (['cobol', 'cob'].includes(langLower)) {
|
|
30
|
+
normalizedLang = 'cobol';
|
|
31
|
+
}
|
|
32
|
+
else if (langLower === 'c') {
|
|
33
|
+
normalizedLang = 'c';
|
|
34
|
+
}
|
|
10
35
|
return Object.assign(snippets, {
|
|
11
36
|
snippets: snippets.snippets.concat([
|
|
12
|
-
{ code: "", fileName, lineNumber, complete: false, skip: skip ?? false }
|
|
37
|
+
{ code: "", language: normalizedLang, fileName, lineNumber, complete: false, skip: skip ?? false, indentation }
|
|
13
38
|
])
|
|
14
39
|
});
|
|
15
40
|
}
|
|
@@ -17,7 +42,11 @@ function addLineToLastSnippet(line) {
|
|
|
17
42
|
return function addLine(snippets) {
|
|
18
43
|
const lastSnippet = snippets.snippets[snippets.snippets.length - 1];
|
|
19
44
|
if (lastSnippet && !lastSnippet.complete) {
|
|
20
|
-
|
|
45
|
+
let lineToAdd = line;
|
|
46
|
+
if (lastSnippet.indentation && line.startsWith(lastSnippet.indentation)) {
|
|
47
|
+
lineToAdd = line.slice(lastSnippet.indentation.length);
|
|
48
|
+
}
|
|
49
|
+
lastSnippet.code += lineToAdd + "\n";
|
|
21
50
|
}
|
|
22
51
|
return snippets;
|
|
23
52
|
};
|
|
@@ -38,8 +67,10 @@ function shareCodeInFile(snippets) {
|
|
|
38
67
|
return snippets;
|
|
39
68
|
}
|
|
40
69
|
function parseLine(line) {
|
|
41
|
-
|
|
42
|
-
|
|
70
|
+
const startMatch = isStartOfSnippet(line);
|
|
71
|
+
if (startMatch) {
|
|
72
|
+
// startMatch[1] is indentation, startMatch[2] is language
|
|
73
|
+
return (snippets, fileName, lineNumber) => startNewSnippet(snippets, fileName, lineNumber, startMatch[2], startMatch[1]);
|
|
43
74
|
}
|
|
44
75
|
if (isEndOfSnippet(line)) {
|
|
45
76
|
return endSnippet;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@doccident/doccident",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"description": "Test all the code in your markdown docs!",
|
|
5
5
|
"main": "dist/doctest.js",
|
|
6
6
|
"files": [
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
"build": "tsc",
|
|
15
15
|
"lint": "eslint .",
|
|
16
16
|
"test": "vitest run",
|
|
17
|
-
"
|
|
17
|
+
"test:readme": "node bin/cmd.js README.md",
|
|
18
|
+
"precommit": "npm run lint && npm run test && npm run test:readme",
|
|
18
19
|
"clean": "rm -rf dist",
|
|
19
20
|
"prepublishOnly": "npm run clean && npm run build"
|
|
20
21
|
},
|