@doccident/doccident 0.0.4 → 0.0.5

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.
@@ -5,36 +5,84 @@ const child_process_1 = require("child_process");
5
5
  const fs_1 = require("fs");
6
6
  const path_1 = require("path");
7
7
  const os_1 = require("os");
8
- const rustHandler = (code, _snippet, _config, _sandbox, _isSharedSandbox) => {
8
+ const rustHandler = (code, _snippet, config, sandbox, isSharedSandbox) => {
9
9
  let success = false;
10
10
  let stack = "";
11
- // Rust execution logic
12
- // Auto-wrap in main function if not present
11
+ const context = sandbox;
13
12
  let rustCode = code;
14
- if (!rustCode.includes('fn main()')) {
15
- rustCode = `fn main() {
16
- ${rustCode}
17
- }`;
13
+ // Handle shared state
14
+ if (isSharedSandbox) {
15
+ if (!context._rustCode) {
16
+ context._rustCode = "";
17
+ }
18
+ context._rustCode += code + "\n";
19
+ rustCode = context._rustCode;
20
+ }
21
+ let finalSource = "";
22
+ // Parse attributes vs body
23
+ // We want to lift crate-level attributes (#![...]) to the top
24
+ // Everything else goes inside main
25
+ // Check if code already has fn main() at the top level?
26
+ // If users provide a full program with main(), wrapping it in another main is weird but valid.
27
+ // However, for shared state, we generally assume "script mode".
28
+ // If not shared, we stick to the old logic (check for main, if not present wrap).
29
+ if (!isSharedSandbox) {
30
+ if (!rustCode.includes('fn main()')) {
31
+ finalSource = `fn main() {
32
+ ${rustCode}
33
+ }`;
34
+ }
35
+ else {
36
+ finalSource = rustCode;
37
+ }
38
+ }
39
+ else {
40
+ // Shared state mode
41
+ const lines = rustCode.split('\n');
42
+ const attributes = [];
43
+ const body = [];
44
+ for (const line of lines) {
45
+ const trimmed = line.trim();
46
+ if (trimmed.startsWith('#![') || trimmed.startsWith('extern crate ')) {
47
+ attributes.push(line);
48
+ }
49
+ else {
50
+ body.push(line);
51
+ }
52
+ }
53
+ finalSource = `${attributes.join('\n')}
54
+
55
+ fn main() {
56
+ ${body.join('\n')}
57
+ }`;
18
58
  }
19
59
  const uniqueId = `${Date.now()}_${Math.random().toString(36).substring(7)}`;
20
60
  const tempSourceFile = (0, path_1.join)((0, os_1.tmpdir)(), `doccident_rust_${uniqueId}.rs`);
21
61
  const tempExeFile = (0, path_1.join)((0, os_1.tmpdir)(), `doccident_rust_${uniqueId}`);
62
+ const timeout = config.timeout || 30000;
22
63
  try {
23
- (0, fs_1.writeFileSync)(tempSourceFile, rustCode);
64
+ (0, fs_1.writeFileSync)(tempSourceFile, finalSource);
24
65
  // Compile
25
- const compileResult = (0, child_process_1.spawnSync)('rustc', [tempSourceFile, '-o', tempExeFile], { encoding: 'utf-8' });
66
+ const compileResult = (0, child_process_1.spawnSync)('rustc', [tempSourceFile, '-o', tempExeFile], { encoding: 'utf-8', timeout });
26
67
  if (compileResult.status !== 0) {
27
68
  stack = compileResult.stderr || "Rust compilation failed";
69
+ if (isSharedSandbox) {
70
+ stack += `\n\nGenerated Source:\n${finalSource}`;
71
+ }
28
72
  }
29
73
  else {
30
74
  // Run
31
- const runResult = (0, child_process_1.spawnSync)(tempExeFile, [], { encoding: 'utf-8' });
75
+ const runResult = (0, child_process_1.spawnSync)(tempExeFile, [], { encoding: 'utf-8', timeout });
76
+ if (runResult.error && runResult.error.code === 'ETIMEDOUT') {
77
+ return { success: false, stack: `Execution timed out after ${timeout}ms` };
78
+ }
32
79
  if (runResult.status === 0) {
33
80
  success = true;
34
81
  }
35
82
  else {
36
83
  stack = runResult.stderr || "Rust execution failed with non-zero exit code";
37
84
  }
85
+ return { success, stack, output: runResult.stdout };
38
86
  }
39
87
  }
40
88
  catch (e) {
@@ -2,7 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.shellHandler = void 0;
4
4
  const child_process_1 = require("child_process");
5
- const shellHandler = (code, snippet, _config, sandbox, isSharedSandbox) => {
5
+ const shellHandler = (code, snippet, config, sandbox, isSharedSandbox) => {
6
6
  let success = false;
7
7
  let stack = "";
8
8
  const context = sandbox;
@@ -19,8 +19,16 @@ const shellHandler = (code, snippet, _config, sandbox, isSharedSandbox) => {
19
19
  code = context._shellContext[shell];
20
20
  }
21
21
  try {
22
+ const args = snippet.args || [];
23
+ const env = snippet.env ? { ...process.env, ...snippet.env } : undefined;
22
24
  // Use the detected shell
23
- const result = (0, child_process_1.spawnSync)(shell, ['-s'], { input: code, encoding: 'utf-8' });
25
+ // shell -s arg1 arg2 reads from stdin and passes args
26
+ const spawnArgs = ['-s', ...args];
27
+ const timeout = config.timeout || 30000;
28
+ const result = (0, child_process_1.spawnSync)(shell, spawnArgs, { input: code, encoding: 'utf-8', env, timeout });
29
+ if (result.error && result.error.code === 'ETIMEDOUT') {
30
+ return { success: false, stack: `Execution timed out after ${timeout}ms` };
31
+ }
24
32
  if (result.status === 0) {
25
33
  success = true;
26
34
  }
@@ -28,10 +36,11 @@ const shellHandler = (code, snippet, _config, sandbox, isSharedSandbox) => {
28
36
  const exitCode = result.status !== null ? result.status : 'signal';
29
37
  stack = result.stderr || result.stdout || `${shell} execution failed with non-zero exit code: ${exitCode}`;
30
38
  }
39
+ return { success, stack, output: result.stdout };
31
40
  }
32
41
  catch (e) {
33
42
  stack = e.message || `Failed to spawn ${shell}`;
43
+ return { success, stack };
34
44
  }
35
- return { success, stack };
36
45
  };
37
46
  exports.shellHandler = shellHandler;
@@ -1,14 +1,28 @@
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;
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|basic|java|perl|pl|csharp|cs|r|pascal|pas|text|txt|output)\s?$/i;
5
5
  const isStartOfSnippet = (line) => line.match(START_REGEX);
6
6
  const isEndOfSnippet = (line) => line.trim() === "```";
7
7
  const isSkip = (line) => line.trim() === "<!-- skip-example -->";
8
8
  const isCodeSharedInFile = (line) => line.trim() === "<!-- share-code-between-examples -->";
9
+ const isId = (line) => line.match(/^<!--\s*id:\s*(\S+)\s*-->$/);
10
+ const isOutputOf = (line) => line.match(/^<!--\s*output:\s*(\S+)(?:\s+(match:regex|match:fuzzy|ignore-whitespace))?\s*-->$/);
11
+ const isArgs = (line) => line.match(/^<!--\s*args:\s*(.+)\s*-->$/);
12
+ const isEnv = (line) => line.match(/^<!--\s*env:\s*(.+)\s*-->$/);
9
13
  function startNewSnippet(snippets, fileName, lineNumber, language, indentation) {
10
14
  const skip = snippets.skip;
11
15
  snippets.skip = false;
16
+ const id = snippets.id;
17
+ snippets.id = undefined;
18
+ const outputOf = snippets.outputOf;
19
+ snippets.outputOf = undefined;
20
+ const outputMode = snippets.outputMode;
21
+ snippets.outputMode = undefined;
22
+ const args = snippets.args;
23
+ snippets.args = undefined;
24
+ const env = snippets.env;
25
+ snippets.env = undefined;
12
26
  let normalizedLang = 'javascript';
13
27
  const langLower = language.toLowerCase();
14
28
  if (['python', 'py'].includes(langLower)) {
@@ -32,9 +46,30 @@ function startNewSnippet(snippets, fileName, lineNumber, language, indentation)
32
46
  else if (langLower === 'c') {
33
47
  normalizedLang = 'c';
34
48
  }
49
+ else if (langLower === 'basic') {
50
+ normalizedLang = 'basic';
51
+ }
52
+ else if (langLower === 'java') {
53
+ normalizedLang = 'java';
54
+ }
55
+ else if (['perl', 'pl'].includes(langLower)) {
56
+ normalizedLang = 'perl';
57
+ }
58
+ else if (['csharp', 'cs'].includes(langLower)) {
59
+ normalizedLang = 'csharp';
60
+ }
61
+ else if (langLower === 'r') {
62
+ normalizedLang = 'r';
63
+ }
64
+ else if (['pascal', 'pas'].includes(langLower)) {
65
+ normalizedLang = 'pascal';
66
+ }
67
+ else if (['text', 'txt', 'output'].includes(langLower)) {
68
+ normalizedLang = 'text';
69
+ }
35
70
  return Object.assign(snippets, {
36
71
  snippets: snippets.snippets.concat([
37
- { code: "", language: normalizedLang, fileName, lineNumber, complete: false, skip: skip ?? false, indentation }
72
+ { code: "", language: normalizedLang, fileName, lineNumber, complete: false, skip: skip ?? false, indentation, id, outputOf, outputMode, args, env }
38
73
  ])
39
74
  });
40
75
  }
@@ -51,10 +86,11 @@ function addLineToLastSnippet(line) {
51
86
  return snippets;
52
87
  };
53
88
  }
54
- function endSnippet(snippets, _fileName, _lineNumber) {
89
+ function endSnippet(snippets, _fileName, lineNumber) {
55
90
  const lastSnippet = snippets.snippets[snippets.snippets.length - 1];
56
91
  if (lastSnippet) {
57
92
  lastSnippet.complete = true;
93
+ lastSnippet.endLine = lineNumber;
58
94
  }
59
95
  return snippets;
60
96
  }
@@ -66,7 +102,60 @@ function shareCodeInFile(snippets) {
66
102
  snippets.shareCodeInFile = true;
67
103
  return snippets;
68
104
  }
105
+ function setId(id) {
106
+ return (snippets) => {
107
+ snippets.id = id;
108
+ return snippets;
109
+ };
110
+ }
111
+ function setOutputOf(id, mode) {
112
+ return (snippets) => {
113
+ snippets.outputOf = id;
114
+ if (mode === 'match:regex') {
115
+ snippets.outputMode = 'regex';
116
+ }
117
+ else if (mode === 'match:fuzzy' || mode === 'ignore-whitespace') {
118
+ snippets.outputMode = 'ignore-whitespace';
119
+ }
120
+ else {
121
+ snippets.outputMode = 'exact';
122
+ }
123
+ return snippets;
124
+ };
125
+ }
126
+ function setArgs(argsStr) {
127
+ return (snippets) => {
128
+ // Simple space splitting, maybe improve for quotes?
129
+ // Assuming simple space separation for now.
130
+ // Filter out empty strings that can occur from leading/trailing whitespace
131
+ snippets.args = argsStr.split(/\s+/).filter(arg => arg.length > 0);
132
+ return snippets;
133
+ };
134
+ }
135
+ function setEnv(envStr) {
136
+ return (snippets) => {
137
+ const env = {};
138
+ // Parse KEY=VALUE pairs separated by spaces
139
+ const pairs = envStr.split(/\s+/);
140
+ for (const pair of pairs) {
141
+ const [key, value] = pair.split('=');
142
+ if (key && value) {
143
+ env[key] = value;
144
+ }
145
+ }
146
+ snippets.env = env;
147
+ return snippets;
148
+ };
149
+ }
69
150
  function parseLine(line) {
151
+ const argsMatch = isArgs(line);
152
+ if (argsMatch) {
153
+ return setArgs(argsMatch[1]);
154
+ }
155
+ const envMatch = isEnv(line);
156
+ if (envMatch) {
157
+ return setEnv(envMatch[1]);
158
+ }
70
159
  const startMatch = isStartOfSnippet(line);
71
160
  if (startMatch) {
72
161
  // startMatch[1] is indentation, startMatch[2] is language
@@ -81,6 +170,16 @@ function parseLine(line) {
81
170
  if (isCodeSharedInFile(line)) {
82
171
  return shareCodeInFile;
83
172
  }
173
+ const idMatch = isId(line);
174
+ if (idMatch) {
175
+ // console.log("Found ID:", idMatch[1]);
176
+ return setId(idMatch[1]);
177
+ }
178
+ const outputMatch = isOutputOf(line);
179
+ if (outputMatch) {
180
+ // console.log("Found OutputOf:", outputMatch[1], "Mode:", outputMatch[2]);
181
+ return setOutputOf(outputMatch[1], outputMatch[2]);
182
+ }
84
183
  return addLineToLastSnippet(line);
85
184
  }
86
185
  function parseCodeSnippets(args) {
package/dist/reporter.js CHANGED
@@ -27,6 +27,45 @@ function printResults(results) {
27
27
  else {
28
28
  console.log(chalk_1.default.red("Failed: " + failingCount));
29
29
  }
30
+ // Summary Table
31
+ console.log("\nSummary Table:");
32
+ const headers = { lang: 'Language', file: 'File', line: 'Line', status: 'Status', time: 'Time (ms)' };
33
+ const rows = results.map(r => {
34
+ return {
35
+ lang: r.codeSnippet.language || 'text',
36
+ file: r.codeSnippet.fileName,
37
+ line: r.codeSnippet.lineNumber.toString(),
38
+ status: r.status === 'pass' ? '✅' : (r.status === 'skip' ? '⏭️' : '❌'),
39
+ time: r.executionTime ? r.executionTime.toFixed(2) : '-'
40
+ };
41
+ });
42
+ const widths = {
43
+ lang: headers.lang.length,
44
+ file: headers.file.length,
45
+ line: headers.line.length,
46
+ status: headers.status.length,
47
+ time: headers.time.length
48
+ };
49
+ rows.forEach(row => {
50
+ widths.lang = Math.max(widths.lang, row.lang.length);
51
+ widths.file = Math.max(widths.file, row.file.length);
52
+ widths.line = Math.max(widths.line, row.line.length);
53
+ // Emojis are tricky. Let's assume they take 2 visual columns.
54
+ // '✅'.length is 1, '❌'.length is 1, '⏭️'.length is 2.
55
+ // We'll trust the string length for now but maybe ensure minimum for status?
56
+ widths.status = Math.max(widths.status, [...row.status].length);
57
+ widths.time = Math.max(widths.time, row.time.length);
58
+ });
59
+ const padRight = (str, width) => str + ' '.repeat(Math.max(0, width - str.length));
60
+ const padLeft = (str, width) => ' '.repeat(Math.max(0, width - str.length)) + str;
61
+ // Header
62
+ console.log(`| ${padRight(headers.lang, widths.lang)} | ${padRight(headers.file, widths.file)} | ${padLeft(headers.line, widths.line)} | ${padRight(headers.status, widths.status)} | ${padLeft(headers.time, widths.time)} |`);
63
+ // Separator
64
+ console.log(`|-${'-'.repeat(widths.lang)}-|-${'-'.repeat(widths.file)}-|-${'-'.repeat(widths.line)}-|-${'-'.repeat(widths.status)}-|-${'-'.repeat(widths.time)}-|`);
65
+ // Rows
66
+ rows.forEach(row => {
67
+ console.log(`| ${padRight(row.lang, widths.lang)} | ${padRight(row.file, widths.file)} | ${padLeft(row.line, widths.line)} | ${padRight(row.status, widths.status)} | ${padLeft(row.time, widths.time)} |`);
68
+ });
30
69
  }
31
70
  function printFailure(result) {
32
71
  console.log(chalk_1.default.red(`Failed - ${markDownErrorLocation(result)}`));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@doccident/doccident",
3
- "version": "0.0.4",
3
+ "version": "0.0.5",
4
4
  "description": "Test all the code in your markdown docs!",
5
5
  "main": "dist/doctest.js",
6
6
  "files": [
@@ -32,7 +32,7 @@
32
32
  "Nick Johnstone",
33
33
  "Billaud Cipher <BillaudCipher@proton.me>"
34
34
  ],
35
- "license": "MIT",
35
+ "license": "Apache-2.0",
36
36
  "bugs": {
37
37
  "url": "https://github.com/BillaudCipher/doccident/issues"
38
38
  },