Skip to content

Commit 3328b93

Browse files
Merge pull request #12 from OpenZeppelin/chore/scripting-updates
chore/scripting updates
2 parents 44d3780 + ab4c4a2 commit 3328b93

6 files changed

Lines changed: 354 additions & 190 deletions

File tree

scripts/convert-adoc.js

Lines changed: 61 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#!/usr/bin/env node
22

33
const fs = require("fs").promises;
4+
const fsSync = require("fs");
45
const path = require("path");
56
const { execSync } = require("child_process");
67
const { glob } = require("glob");
@@ -61,7 +62,7 @@ async function convertAdocFiles(directory) {
6162
await fs.writeFile(tempFile, content, "utf8");
6263

6364
// Run downdoc
64-
execSync(`bunx downdoc "${tempFile}"`, { stdio: "pipe" });
65+
execSync(`pnpm dlx downdoc "${tempFile}"`, { stdio: "pipe" });
6566

6667
// Find the generated .md file
6768
const tempMdFile = path.join(dir, `temp_${filename}.md`);
@@ -91,70 +92,67 @@ async function convertAdocFiles(directory) {
9192
// Fix xref: links - remove xref: and convert .adoc to .mdx
9293
mdContent = mdContent.replace(
9394
/xref:\[([^\]]+)\]\(([^)]+)\)/g,
94-
"[$1]($2)"
95+
"[$1]($2)",
9596
);
9697

9798
// Fix .adoc internal links to .mdx
9899
mdContent = mdContent.replace(
99100
/\]\(([^)]+)\.adoc([^)]*)\)/g,
100-
"]($1.mdx$2)"
101+
"]($1.mdx$2)",
101102
);
102103

103104
// Fix curly bracket file references {filename} -> filename
104-
mdContent = mdContent.replace(
105-
/\{([^}]+)\}/g,
106-
"$1"
107-
);
105+
mdContent = mdContent.replace(/\{([^}]+)\}/g, "$1");
108106

109107
// Fix HTML-style callouts <dl><dt><strong>📌 NOTE</strong></dt><dd> ... </dd></dl>
110108
// Handle multi-line callouts by using a more permissive pattern
111109
mdContent = mdContent.replace(
112-
/<dl><dt><strong>[📌🔔]\s*(NOTE|TIP|INFO)<\/strong><\/dt><dd>([\s\S]*?)<\/dd><\/dl>/g,
113-
"<Callout>\n$2\n</Callout>"
110+
/<dl><dt><strong>[📌🔔]\s*(NOTE|TIP|INFO)<\/strong><\/dt><dd>([\s\S]*?)<\/dd><\/dl>/gu,
111+
"<Callout>\n$2\n</Callout>",
114112
);
115113

116114
mdContent = mdContent.replace(
117115
/<dl><dt><strong>[🚨]\s*(WARNING|IMPORTANT|CAUTION|DANGER)<\/strong><\/dt><dd>([\s\S]*?)<\/dd><\/dl>/g,
118-
"<Callout type='warn'>\n$2\n</Callout>"
116+
"<Callout type='warn'>\n$2\n</Callout>",
119117
);
120118

121119
// Handle cases where </dd></dl> might be missing or malformed
122120
mdContent = mdContent.replace(
123-
/<dl><dt><strong>[📌🔔]\s*(NOTE|TIP|INFO)<\/strong><\/dt><dd>([\s\S]*?)(?=\n\n|<dl>|$)/g,
124-
"<Callout>\n$2\n</Callout>"
121+
/<dl><dt><strong>[📌🔔]\s*(NOTE|TIP|INFO)<\/strong><\/dt><dd>([\s\S]*?)(?=\n\n|<dl>|$)/gu,
122+
"<Callout>\n$2\n</Callout>",
125123
);
126124

127125
mdContent = mdContent.replace(
128126
/<dl><dt><strong>[🚨]\s*(WARNING|IMPORTANT|CAUTION|DANGER)<\/strong><\/dt><dd>([\s\S]*?)(?=\n\n|<dl>|$)/g,
129-
"<Callout type='warn'>\n$2\n</Callout>"
127+
"<Callout type='warn'>\n$2\n</Callout>",
130128
);
131129

132130
// Fix xref patterns with complex anchors like xref:#ISRC6-\\__execute__[...]
133131
mdContent = mdContent.replace(
134132
/xref:#([^[\]]+)\[([^\]]+)\]/g,
135-
"[$2](#$1)"
133+
"[$2](#$1)",
136134
);
137135

138136
// Fix simple xref patterns
139-
mdContent = mdContent.replace(
140-
/xref:([^[\s]+)\[([^\]]+)\]/g,
141-
"[$2]($1)"
142-
);
137+
mdContent = mdContent.replace(/xref:([^[\s]+)\[([^\]]+)\]/g, "[$2]($1)");
143138

144139
// Clean up orphaned HTML tags from malformed callouts
145140
// Handle orphaned <dl><dt><strong>EMOJI TYPE</strong></dt><dd> without closing tags
146141
mdContent = mdContent.replace(
147-
/<dl><dt><strong>[📌🔔]\s*(NOTE|TIP|INFO)<\/strong><\/dt><dd>\s*\n([\s\S]*?)(?=\n\n|<dl>|$)/g,
148-
"<Callout>\n$2\n</Callout>"
142+
/<dl><dt><strong>[📌🔔]\s*(NOTE|TIP|INFO)<\/strong><\/dt><dd>\s*\n([\s\S]*?)(?=\n\n|<dl>|$)/gu,
143+
"<Callout>\n$2\n</Callout>",
149144
);
150145

151146
mdContent = mdContent.replace(
152147
/<dl><dt><strong>[🚨]\s*(WARNING|IMPORTANT|CAUTION|DANGER)<\/strong><\/dt><dd>\s*\n([\s\S]*?)(?=\n\n|<dl>|$)/g,
153-
"<Callout type='warn'>\n$2\n</Callout>"
148+
"<Callout type='warn'>\n$2\n</Callout>",
154149
);
155150

156151
// Clean up any remaining orphaned HTML tags
157-
mdContent = mdContent.replace(/<dl><dt><strong>.*?<\/strong><\/dt><dd>/g, "");
152+
mdContent = mdContent.replace(
153+
/<dl><dt><strong>.*?<\/strong><\/dt><dd>/g,
154+
"",
155+
);
158156
mdContent = mdContent.replace(/<\/dd><\/dl>/g, "");
159157
mdContent = mdContent.replace(/<dd>/g, "");
160158
mdContent = mdContent.replace(/<\/dd>/g, "");
@@ -172,12 +170,13 @@ async function convertAdocFiles(directory) {
172170
const title = headerMatch ? headerMatch[1].trim() : filename;
173171

174172
// Remove the first H1 from content
175-
const contentWithoutFirstH1 = mdContent.replace(/^#+\s+.+$/m, '').replace(/^\n+/, '');
173+
const contentWithoutFirstH1 = mdContent
174+
.replace(/^#+\s+.+$/m, "")
175+
.replace(/^\n+/, "");
176176

177177
// Create MDX with frontmatter
178178
const mdxContent = `---
179179
title: ${title}
180-
description: ${title}
181180
---
182181
183182
${contentWithoutFirstH1}`;
@@ -196,5 +195,43 @@ ${contentWithoutFirstH1}`;
196195
}
197196
}
198197

198+
// Process files to remove curly brackets after conversion
199+
function processFile(filePath) {
200+
try {
201+
const content = fsSync.readFileSync(filePath, "utf8");
202+
// Preserve brackets inside code fences (```...```)
203+
const modifiedContent = content.replace(/```[\s\S]*?```|[{}]/g, (match) => {
204+
// If match contains newlines or starts with ```, it's a code block - preserve it
205+
return match.includes("\n") || match.startsWith("```") ? match : "";
206+
});
207+
fsSync.writeFileSync(filePath, modifiedContent, "utf8");
208+
console.log(`Processed: ${filePath}`);
209+
} catch (error) {
210+
console.error(`Error processing ${filePath}: ${error.message}`);
211+
}
212+
}
213+
214+
function crawlDirectory(dirPath) {
215+
try {
216+
const items = fsSync.readdirSync(dirPath);
217+
218+
for (const item of items) {
219+
const itemPath = path.join(dirPath, item);
220+
const stats = fsSync.statSync(itemPath);
221+
222+
if (stats.isDirectory()) {
223+
crawlDirectory(itemPath);
224+
} else if (stats.isFile()) {
225+
processFile(itemPath);
226+
}
227+
}
228+
} catch (error) {
229+
console.error(`Error crawling directory ${dirPath}: ${error.message}`);
230+
}
231+
}
232+
199233
const directory = process.argv[2];
200234
convertAdocFiles(directory).catch(console.error);
235+
236+
// Run bracket processing after conversion
237+
crawlDirectory(directory);

scripts/generate-api-docs.js

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
#!/usr/bin/env node
2+
3+
const fs = require("node:fs").promises;
4+
const path = require("node:path");
5+
const { execSync } = require("node:child_process");
6+
7+
// Parse command line arguments
8+
function parseArgs() {
9+
const args = process.argv.slice(2);
10+
const options = {
11+
contractsRepo:
12+
"https://github.com/stevedylandev/openzeppelin-contracts.git",
13+
contractsBranch: "master",
14+
tempDir: "temp-contracts",
15+
apiOutputDir: "content/contracts/v5.x/api",
16+
examplesOutputDir: "examples",
17+
};
18+
19+
for (let i = 0; i < args.length; i++) {
20+
const arg = args[i];
21+
switch (arg) {
22+
case "--help":
23+
case "-h":
24+
showHelp();
25+
process.exit(0);
26+
break;
27+
case "--repo":
28+
case "-r":
29+
options.contractsRepo = args[++i];
30+
break;
31+
case "--branch":
32+
case "-b":
33+
options.contractsBranch = args[++i];
34+
break;
35+
case "--temp-dir":
36+
case "-t":
37+
options.tempDir = args[++i];
38+
break;
39+
case "--api-output":
40+
case "-a":
41+
options.apiOutputDir = args[++i];
42+
break;
43+
case "--examples-output":
44+
case "-e":
45+
options.examplesOutputDir = args[++i];
46+
break;
47+
default:
48+
console.error(`Unknown option: ${arg}`);
49+
showHelp();
50+
process.exit(1);
51+
}
52+
}
53+
54+
return options;
55+
}
56+
57+
function showHelp() {
58+
console.log(`
59+
Generate OpenZeppelin Contracts API documentation
60+
61+
Usage: node generate-api-docs.js [options]
62+
63+
Options:
64+
-r, --repo <url> Contracts repository URL (default: https://github.com/OpenZeppelin/openzeppelin-contracts.git)
65+
-b, --branch <branch> Contracts repository branch (default: master)
66+
-t, --temp-dir <dir> Temporary directory for cloning (default: temp-contracts)
67+
-a, --api-output <dir> API documentation output directory (default: content/contracts/v5.x/api)
68+
-e, --examples-output <dir> Examples output directory (default: examples)
69+
-h, --help Show this help message
70+
71+
Examples:
72+
node generate-api-docs.js
73+
node generate-api-docs.js --repo https://github.com/myorg/contracts.git --branch v4.0
74+
node generate-api-docs.js --api-output content/contracts/v4.x/api --examples-output examples-v4
75+
`);
76+
}
77+
78+
async function generateApiDocs(options) {
79+
const {
80+
contractsRepo,
81+
contractsBranch,
82+
tempDir,
83+
apiOutputDir,
84+
examplesOutputDir,
85+
} = options;
86+
87+
console.log("🔄 Generating OpenZeppelin Contracts API documentation...");
88+
console.log(`📦 Repository: ${contractsRepo}`);
89+
console.log(`🌿 Branch: ${contractsBranch}`);
90+
console.log(`📂 API Output: ${apiOutputDir}`);
91+
console.log(`📂 Examples Output: ${examplesOutputDir}`);
92+
93+
try {
94+
// Clean up previous runs
95+
console.log("🧹 Cleaning up previous runs...");
96+
await fs.rm(tempDir, { recursive: true, force: true });
97+
await fs.rm(apiOutputDir, { recursive: true, force: true });
98+
99+
// Create output directory
100+
await fs.mkdir(apiOutputDir, { recursive: true });
101+
102+
// Clone the contracts repository
103+
console.log("📦 Cloning contracts repository...");
104+
execSync(
105+
`git clone --depth 1 --branch "${contractsBranch}" --recurse-submodules "${contractsRepo}" "${tempDir}"`,
106+
{
107+
stdio: "inherit",
108+
},
109+
);
110+
111+
// Navigate to contracts directory and install dependencies
112+
console.log("📚 Installing dependencies...");
113+
const originalDir = process.cwd();
114+
process.chdir(tempDir);
115+
116+
try {
117+
execSync("npm install --silent", { stdio: "inherit" });
118+
119+
// Generate markdown documentation
120+
console.log("🏗️ Generating clean markdown documentation...");
121+
execSync("npm run prepare-docs", { stdio: "inherit" });
122+
123+
// Copy generated markdown files
124+
console.log("📋 Copying generated documentation...");
125+
const docsPath = path.join("docs", "modules", "api", "pages");
126+
127+
try {
128+
await fs.access(docsPath);
129+
// Copy API docs
130+
const apiSource = path.join(process.cwd(), docsPath);
131+
const apiDest = path.join(originalDir, apiOutputDir);
132+
await copyDirRecursive(apiSource, apiDest);
133+
console.log(`✅ API documentation copied to ${apiOutputDir}`);
134+
} catch (error) {
135+
console.log(
136+
"❌ Error: Markdown documentation not found at expected location",
137+
);
138+
process.exit(1);
139+
}
140+
141+
// Copy examples if they exist
142+
const examplesPath = path.join("docs", "modules", "api", "examples");
143+
if (
144+
await fs
145+
.access(examplesPath)
146+
.then(() => true)
147+
.catch(() => false)
148+
) {
149+
const examplesDest = path.join(originalDir, examplesOutputDir);
150+
await fs.mkdir(examplesDest, { recursive: true });
151+
await copyDirRecursive(
152+
path.join(process.cwd(), examplesPath),
153+
examplesDest,
154+
);
155+
console.log(`✅ Examples copied to ${examplesOutputDir}`);
156+
}
157+
158+
// Get version for index file
159+
let version = "latest";
160+
try {
161+
const packageJson = JSON.parse(
162+
await fs.readFile("package.json", "utf8"),
163+
);
164+
version = packageJson.version || version;
165+
} catch (error) {
166+
console.log("⚠️ Could not read package.json for version info");
167+
}
168+
169+
// Generate index file
170+
console.log("📝 Generating API index...");
171+
const indexContent = `---
172+
title: API Reference
173+
---
174+
175+
# API Reference
176+
`;
177+
178+
await fs.writeFile(
179+
path.join(originalDir, apiOutputDir, "index.mdx"),
180+
indexContent,
181+
"utf8",
182+
);
183+
} finally {
184+
// Go back to original directory
185+
process.chdir(originalDir);
186+
}
187+
188+
// Clean up temporary directory
189+
console.log("🧹 Cleaning up...");
190+
await fs.rm(tempDir, { recursive: true, force: true });
191+
192+
console.log("🎉 API documentation generation complete!");
193+
console.log(`📂 Documentation available in: ${apiOutputDir}`);
194+
console.log("");
195+
console.log("Next steps:");
196+
console.log(` - Review generated markdown files in ${apiOutputDir}`);
197+
console.log(" - Update the api/index.mdx file with your TOC");
198+
} catch (error) {
199+
console.error("❌ Error generating API documentation:", error.message);
200+
process.exit(1);
201+
}
202+
}
203+
204+
async function copyDirRecursive(src, dest) {
205+
const entries = await fs.readdir(src, { withFileTypes: true });
206+
207+
for (const entry of entries) {
208+
const srcPath = path.join(src, entry.name);
209+
const destPath = path.join(dest, entry.name);
210+
211+
if (entry.isDirectory()) {
212+
await fs.mkdir(destPath, { recursive: true });
213+
await copyDirRecursive(srcPath, destPath);
214+
} else {
215+
await fs.copyFile(srcPath, destPath);
216+
}
217+
}
218+
}
219+
220+
// Main execution
221+
const options = parseArgs();
222+
generateApiDocs(options);

0 commit comments

Comments
 (0)