@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 CHANGED
@@ -1,17 +1,30 @@
1
1
  # doccident
2
2
 
3
3
  [![npm version](https://badge.fury.io/js/doccident.svg)](http://badge.fury.io/js/doccident)
4
+ [![Doccident Tested](https://img.shields.io/badge/doccident-tested-brightgreen.svg)](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 JavaScript and TypeScript code blocks, and executes them in a sandboxed environment to verify they run without errors.
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 `js`, `javascript`, `es6`, `ts`, or `typescript` fenced code blocks. It automatically transforms modern JavaScript and TypeScript using **esbuild** before execution.
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
- * **TypeScript Support**: Now recognizes `ts` and `typescript` blocks in addition to JS.
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. This prevents examples from polluting the global scope of the runner itself, while allowing controlled injection of dependencies via the configuration.
136
- * **Transformation**: Uses **esbuild** to compile modern JavaScript and TypeScript code down to a compatible format before execution. This ensures that modern syntax and type annotations run correctly in all environments, and is significantly faster than the previous Babel implementation.
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
- try {
88
- const result = (0, esbuild_1.transformSync)(code, {
89
- loader: 'ts',
90
- format: 'cjs',
91
- target: 'node12'
92
- });
93
- code = result.code || "";
94
- (0, vm_1.runInNewContext)(code, perSnippetSandbox || sandbox);
95
- success = true;
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
- catch (e) {
98
- stack = e.stack || "";
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,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -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
- const isStartOfSnippet = (line) => line.trim().match(/```\W*(JavaScript|js|es6|ts|typescript)\s?$/i);
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
- lastSnippet.code += line + "\n";
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
- if (isStartOfSnippet(line)) {
42
- return startNewSnippet;
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",
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
- "precommit": "npm run lint && npm run test",
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
  },