@doccident/doccident 0.0.3 → 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.
@@ -0,0 +1,104 @@
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
+ const context = sandbox;
12
+ let rustCode = code;
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
+ }`;
58
+ }
59
+ const uniqueId = `${Date.now()}_${Math.random().toString(36).substring(7)}`;
60
+ const tempSourceFile = (0, path_1.join)((0, os_1.tmpdir)(), `doccident_rust_${uniqueId}.rs`);
61
+ const tempExeFile = (0, path_1.join)((0, os_1.tmpdir)(), `doccident_rust_${uniqueId}`);
62
+ const timeout = config.timeout || 30000;
63
+ try {
64
+ (0, fs_1.writeFileSync)(tempSourceFile, finalSource);
65
+ // Compile
66
+ const compileResult = (0, child_process_1.spawnSync)('rustc', [tempSourceFile, '-o', tempExeFile], { encoding: 'utf-8', timeout });
67
+ if (compileResult.status !== 0) {
68
+ stack = compileResult.stderr || "Rust compilation failed";
69
+ if (isSharedSandbox) {
70
+ stack += `\n\nGenerated Source:\n${finalSource}`;
71
+ }
72
+ }
73
+ else {
74
+ // Run
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
+ }
79
+ if (runResult.status === 0) {
80
+ success = true;
81
+ }
82
+ else {
83
+ stack = runResult.stderr || "Rust execution failed with non-zero exit code";
84
+ }
85
+ return { success, stack, output: runResult.stdout };
86
+ }
87
+ }
88
+ catch (e) {
89
+ stack = e.message || "Failed to execute rustc or run binary";
90
+ }
91
+ finally {
92
+ try {
93
+ if ((0, fs_1.existsSync)(tempSourceFile))
94
+ (0, fs_1.unlinkSync)(tempSourceFile);
95
+ if ((0, fs_1.existsSync)(tempExeFile))
96
+ (0, fs_1.unlinkSync)(tempExeFile);
97
+ }
98
+ catch {
99
+ // Ignore cleanup error
100
+ }
101
+ }
102
+ return { success, stack };
103
+ };
104
+ exports.rustHandler = rustHandler;
@@ -0,0 +1,46 @@
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
+ const args = snippet.args || [];
23
+ const env = snippet.env ? { ...process.env, ...snippet.env } : undefined;
24
+ // Use the detected shell
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
+ }
32
+ if (result.status === 0) {
33
+ success = true;
34
+ }
35
+ else {
36
+ const exitCode = result.status !== null ? result.status : 'signal';
37
+ stack = result.stderr || result.stdout || `${shell} execution failed with non-zero exit code: ${exitCode}`;
38
+ }
39
+ return { success, stack, output: result.stdout };
40
+ }
41
+ catch (e) {
42
+ stack = e.message || `Failed to spawn ${shell}`;
43
+ return { success, stack };
44
+ }
45
+ };
46
+ exports.shellHandler = shellHandler;
@@ -1,15 +1,75 @@
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|basic|java|perl|pl|csharp|cs|r|pascal|pas|text|txt|output)\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
+ 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*-->$/);
13
+ function startNewSnippet(snippets, fileName, lineNumber, language, indentation) {
8
14
  const skip = snippets.skip;
9
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;
26
+ let normalizedLang = 'javascript';
27
+ const langLower = language.toLowerCase();
28
+ if (['python', 'py'].includes(langLower)) {
29
+ normalizedLang = 'python';
30
+ }
31
+ else if (['bash', 'sh', 'zsh', 'shell'].includes(langLower)) {
32
+ normalizedLang = langLower === 'shell' ? 'bash' : langLower;
33
+ }
34
+ else if (langLower === 'go') {
35
+ normalizedLang = 'go';
36
+ }
37
+ else if (['rust', 'rs'].includes(langLower)) {
38
+ normalizedLang = 'rust';
39
+ }
40
+ else if (['fortran', 'f90', 'f95'].includes(langLower)) {
41
+ normalizedLang = 'fortran';
42
+ }
43
+ else if (['cobol', 'cob'].includes(langLower)) {
44
+ normalizedLang = 'cobol';
45
+ }
46
+ else if (langLower === 'c') {
47
+ normalizedLang = 'c';
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
+ }
10
70
  return Object.assign(snippets, {
11
71
  snippets: snippets.snippets.concat([
12
- { code: "", fileName, lineNumber, complete: false, skip: skip ?? false }
72
+ { code: "", language: normalizedLang, fileName, lineNumber, complete: false, skip: skip ?? false, indentation, id, outputOf, outputMode, args, env }
13
73
  ])
14
74
  });
15
75
  }
@@ -17,15 +77,20 @@ function addLineToLastSnippet(line) {
17
77
  return function addLine(snippets) {
18
78
  const lastSnippet = snippets.snippets[snippets.snippets.length - 1];
19
79
  if (lastSnippet && !lastSnippet.complete) {
20
- lastSnippet.code += line + "\n";
80
+ let lineToAdd = line;
81
+ if (lastSnippet.indentation && line.startsWith(lastSnippet.indentation)) {
82
+ lineToAdd = line.slice(lastSnippet.indentation.length);
83
+ }
84
+ lastSnippet.code += lineToAdd + "\n";
21
85
  }
22
86
  return snippets;
23
87
  };
24
88
  }
25
- function endSnippet(snippets, _fileName, _lineNumber) {
89
+ function endSnippet(snippets, _fileName, lineNumber) {
26
90
  const lastSnippet = snippets.snippets[snippets.snippets.length - 1];
27
91
  if (lastSnippet) {
28
92
  lastSnippet.complete = true;
93
+ lastSnippet.endLine = lineNumber;
29
94
  }
30
95
  return snippets;
31
96
  }
@@ -37,9 +102,64 @@ function shareCodeInFile(snippets) {
37
102
  snippets.shareCodeInFile = true;
38
103
  return snippets;
39
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
+ }
40
150
  function parseLine(line) {
41
- if (isStartOfSnippet(line)) {
42
- return startNewSnippet;
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
+ }
159
+ const startMatch = isStartOfSnippet(line);
160
+ if (startMatch) {
161
+ // startMatch[1] is indentation, startMatch[2] is language
162
+ return (snippets, fileName, lineNumber) => startNewSnippet(snippets, fileName, lineNumber, startMatch[2], startMatch[1]);
43
163
  }
44
164
  if (isEndOfSnippet(line)) {
45
165
  return endSnippet;
@@ -50,6 +170,16 @@ function parseLine(line) {
50
170
  if (isCodeSharedInFile(line)) {
51
171
  return shareCodeInFile;
52
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
+ }
53
183
  return addLineToLastSnippet(line);
54
184
  }
55
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.3",
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": [
@@ -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
  },
@@ -31,7 +32,7 @@
31
32
  "Nick Johnstone",
32
33
  "Billaud Cipher <BillaudCipher@proton.me>"
33
34
  ],
34
- "license": "MIT",
35
+ "license": "Apache-2.0",
35
36
  "bugs": {
36
37
  "url": "https://github.com/BillaudCipher/doccident/issues"
37
38
  },