Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 61 additions & 24 deletions scripts/convert-adoc.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#!/usr/bin/env node

const fs = require("fs").promises;
const fsSync = require("fs");
const path = require("path");
const { execSync } = require("child_process");
const { glob } = require("glob");
Expand Down Expand Up @@ -61,7 +62,7 @@ async function convertAdocFiles(directory) {
await fs.writeFile(tempFile, content, "utf8");

// Run downdoc
execSync(`bunx downdoc "${tempFile}"`, { stdio: "pipe" });
execSync(`pnpm dlx downdoc "${tempFile}"`, { stdio: "pipe" });

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

// Fix .adoc internal links to .mdx
mdContent = mdContent.replace(
/\]\(([^)]+)\.adoc([^)]*)\)/g,
"]($1.mdx$2)"
"]($1.mdx$2)",
);

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

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

mdContent = mdContent.replace(
/<dl><dt><strong>[⚠️🚨❗]\s*(WARNING|IMPORTANT|CAUTION|DANGER)<\/strong><\/dt><dd>([\s\S]*?)<\/dd><\/dl>/g,
"<Callout type='warn'>\n$2\n</Callout>"
"<Callout type='warn'>\n$2\n</Callout>",
);

// Handle cases where </dd></dl> might be missing or malformed
mdContent = mdContent.replace(
/<dl><dt><strong>[📌🔔ℹ️]\s*(NOTE|TIP|INFO)<\/strong><\/dt><dd>([\s\S]*?)(?=\n\n|<dl>|$)/g,
"<Callout>\n$2\n</Callout>"
/<dl><dt><strong>[📌🔔ℹ️]\s*(NOTE|TIP|INFO)<\/strong><\/dt><dd>([\s\S]*?)(?=\n\n|<dl>|$)/gu,
"<Callout>\n$2\n</Callout>",
);

mdContent = mdContent.replace(
/<dl><dt><strong>[⚠️🚨❗]\s*(WARNING|IMPORTANT|CAUTION|DANGER)<\/strong><\/dt><dd>([\s\S]*?)(?=\n\n|<dl>|$)/g,
"<Callout type='warn'>\n$2\n</Callout>"
"<Callout type='warn'>\n$2\n</Callout>",
);

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

// Fix simple xref patterns
mdContent = mdContent.replace(
/xref:([^[\s]+)\[([^\]]+)\]/g,
"[$2]($1)"
);
mdContent = mdContent.replace(/xref:([^[\s]+)\[([^\]]+)\]/g, "[$2]($1)");

// Clean up orphaned HTML tags from malformed callouts
// Handle orphaned <dl><dt><strong>EMOJI TYPE</strong></dt><dd> without closing tags
mdContent = mdContent.replace(
/<dl><dt><strong>[📌🔔ℹ️]\s*(NOTE|TIP|INFO)<\/strong><\/dt><dd>\s*\n([\s\S]*?)(?=\n\n|<dl>|$)/g,
"<Callout>\n$2\n</Callout>"
/<dl><dt><strong>[📌🔔ℹ️]\s*(NOTE|TIP|INFO)<\/strong><\/dt><dd>\s*\n([\s\S]*?)(?=\n\n|<dl>|$)/gu,
"<Callout>\n$2\n</Callout>",
);

mdContent = mdContent.replace(
/<dl><dt><strong>[⚠️🚨❗]\s*(WARNING|IMPORTANT|CAUTION|DANGER)<\/strong><\/dt><dd>\s*\n([\s\S]*?)(?=\n\n|<dl>|$)/g,
"<Callout type='warn'>\n$2\n</Callout>"
"<Callout type='warn'>\n$2\n</Callout>",
);

// Clean up any remaining orphaned HTML tags
mdContent = mdContent.replace(/<dl><dt><strong>.*?<\/strong><\/dt><dd>/g, "");
mdContent = mdContent.replace(
/<dl><dt><strong>.*?<\/strong><\/dt><dd>/g,
"",
);
mdContent = mdContent.replace(/<\/dd><\/dl>/g, "");
mdContent = mdContent.replace(/<dd>/g, "");
mdContent = mdContent.replace(/<\/dd>/g, "");
Expand All @@ -172,12 +170,13 @@ async function convertAdocFiles(directory) {
const title = headerMatch ? headerMatch[1].trim() : filename;

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

// Create MDX with frontmatter
const mdxContent = `---
title: ${title}
description: ${title}
---

${contentWithoutFirstH1}`;
Expand All @@ -196,5 +195,43 @@ ${contentWithoutFirstH1}`;
}
}

// Process files to remove curly brackets after conversion
function processFile(filePath) {
try {
const content = fsSync.readFileSync(filePath, "utf8");
// Preserve brackets inside code fences (```...```)
const modifiedContent = content.replace(/```[\s\S]*?```|[{}]/g, (match) => {
// If match contains newlines or starts with ```, it's a code block - preserve it
return match.includes("\n") || match.startsWith("```") ? match : "";
});
fsSync.writeFileSync(filePath, modifiedContent, "utf8");
console.log(`Processed: ${filePath}`);
} catch (error) {
console.error(`Error processing ${filePath}: ${error.message}`);
}
}

function crawlDirectory(dirPath) {
try {
const items = fsSync.readdirSync(dirPath);

for (const item of items) {
const itemPath = path.join(dirPath, item);
const stats = fsSync.statSync(itemPath);

if (stats.isDirectory()) {
crawlDirectory(itemPath);
} else if (stats.isFile()) {
processFile(itemPath);
}
}
} catch (error) {
console.error(`Error crawling directory ${dirPath}: ${error.message}`);
}
}

const directory = process.argv[2];
convertAdocFiles(directory).catch(console.error);

// Run bracket processing after conversion
crawlDirectory(directory);
222 changes: 222 additions & 0 deletions scripts/generate-api-docs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
#!/usr/bin/env node

const fs = require("node:fs").promises;
const path = require("node:path");
const { execSync } = require("node:child_process");

// Parse command line arguments
function parseArgs() {
const args = process.argv.slice(2);
const options = {
contractsRepo:
"https://github.com/stevedylandev/openzeppelin-contracts.git",
contractsBranch: "master",
tempDir: "temp-contracts",
apiOutputDir: "content/contracts/v5.x/api",
examplesOutputDir: "examples",
};

for (let i = 0; i < args.length; i++) {
const arg = args[i];
switch (arg) {
case "--help":
case "-h":
showHelp();
process.exit(0);
break;
case "--repo":
case "-r":
options.contractsRepo = args[++i];
break;
case "--branch":
case "-b":
options.contractsBranch = args[++i];
break;
case "--temp-dir":
case "-t":
options.tempDir = args[++i];
break;
case "--api-output":
case "-a":
options.apiOutputDir = args[++i];
break;
case "--examples-output":
case "-e":
options.examplesOutputDir = args[++i];
break;
default:
console.error(`Unknown option: ${arg}`);
showHelp();
process.exit(1);
}
}

return options;
}

function showHelp() {
console.log(`
Generate OpenZeppelin Contracts API documentation

Usage: node generate-api-docs.js [options]

Options:
-r, --repo <url> Contracts repository URL (default: https://github.com/OpenZeppelin/openzeppelin-contracts.git)
-b, --branch <branch> Contracts repository branch (default: master)
-t, --temp-dir <dir> Temporary directory for cloning (default: temp-contracts)
-a, --api-output <dir> API documentation output directory (default: content/contracts/v5.x/api)
-e, --examples-output <dir> Examples output directory (default: examples)
-h, --help Show this help message

Examples:
node generate-api-docs.js
node generate-api-docs.js --repo https://github.com/myorg/contracts.git --branch v4.0
node generate-api-docs.js --api-output content/contracts/v4.x/api --examples-output examples-v4
`);
}

async function generateApiDocs(options) {
const {
contractsRepo,
contractsBranch,
tempDir,
apiOutputDir,
examplesOutputDir,
} = options;

console.log("🔄 Generating OpenZeppelin Contracts API documentation...");
console.log(`📦 Repository: ${contractsRepo}`);
console.log(`🌿 Branch: ${contractsBranch}`);
console.log(`📂 API Output: ${apiOutputDir}`);
console.log(`📂 Examples Output: ${examplesOutputDir}`);

try {
// Clean up previous runs
console.log("🧹 Cleaning up previous runs...");
await fs.rm(tempDir, { recursive: true, force: true });
await fs.rm(apiOutputDir, { recursive: true, force: true });

// Create output directory
await fs.mkdir(apiOutputDir, { recursive: true });

// Clone the contracts repository
console.log("📦 Cloning contracts repository...");
execSync(
`git clone --depth 1 --branch "${contractsBranch}" --recurse-submodules "${contractsRepo}" "${tempDir}"`,
{
stdio: "inherit",
},
);

// Navigate to contracts directory and install dependencies
console.log("📚 Installing dependencies...");
const originalDir = process.cwd();
process.chdir(tempDir);

try {
execSync("npm install --silent", { stdio: "inherit" });

// Generate markdown documentation
console.log("🏗️ Generating clean markdown documentation...");
execSync("npm run prepare-docs", { stdio: "inherit" });

// Copy generated markdown files
console.log("📋 Copying generated documentation...");
const docsPath = path.join("docs", "modules", "api", "pages");

try {
await fs.access(docsPath);
// Copy API docs
const apiSource = path.join(process.cwd(), docsPath);
const apiDest = path.join(originalDir, apiOutputDir);
await copyDirRecursive(apiSource, apiDest);
console.log(`✅ API documentation copied to ${apiOutputDir}`);
} catch (error) {
console.log(
"❌ Error: Markdown documentation not found at expected location",
);
process.exit(1);
}

// Copy examples if they exist
const examplesPath = path.join("docs", "modules", "api", "examples");
if (
await fs
.access(examplesPath)
.then(() => true)
.catch(() => false)
) {
const examplesDest = path.join(originalDir, examplesOutputDir);
await fs.mkdir(examplesDest, { recursive: true });
await copyDirRecursive(
path.join(process.cwd(), examplesPath),
examplesDest,
);
console.log(`✅ Examples copied to ${examplesOutputDir}`);
}

// Get version for index file
let version = "latest";
try {
const packageJson = JSON.parse(
await fs.readFile("package.json", "utf8"),
);
version = packageJson.version || version;
} catch (error) {
console.log("⚠️ Could not read package.json for version info");
}

// Generate index file
console.log("📝 Generating API index...");
const indexContent = `---
title: API Reference
---

# API Reference
`;

await fs.writeFile(
path.join(originalDir, apiOutputDir, "index.mdx"),
indexContent,
"utf8",
);
} finally {
// Go back to original directory
process.chdir(originalDir);
}

// Clean up temporary directory
console.log("🧹 Cleaning up...");
await fs.rm(tempDir, { recursive: true, force: true });

console.log("🎉 API documentation generation complete!");
console.log(`📂 Documentation available in: ${apiOutputDir}`);
console.log("");
console.log("Next steps:");
console.log(` - Review generated markdown files in ${apiOutputDir}`);
console.log(" - Update the api/index.mdx file with your TOC");
} catch (error) {
console.error("❌ Error generating API documentation:", error.message);
process.exit(1);
}
}

async function copyDirRecursive(src, dest) {
const entries = await fs.readdir(src, { withFileTypes: true });

for (const entry of entries) {
const srcPath = path.join(src, entry.name);
const destPath = path.join(dest, entry.name);

if (entry.isDirectory()) {
await fs.mkdir(destPath, { recursive: true });
await copyDirRecursive(srcPath, destPath);
} else {
await fs.copyFile(srcPath, destPath);
}
}
}

// Main execution
const options = parseArgs();
generateApiDocs(options);
Loading