@confytome/html
Version:
Standalone HTML generator - creates professional, responsive HTML documentation from OpenAPI specs with modern styling and navigation.
483 lines (418 loc) • 12.7 kB
JavaScript
/**
* Standalone HTML Generator
*
* Truly independent implementation with zero external dependencies.
* Can run via "npx @confytome/html" without any core package dependencies.
*/
import { StandaloneBase } from '@confytome/core/utils/StandaloneBase.js';
import { OpenApiProcessor } from '@confytome/core/utils/OpenApiProcessor.js';
export class StandaloneHtmlGenerator extends StandaloneBase {
constructor(outputDir = './confytome', options = {}) {
super(outputDir, options);
this.processor = null;
}
/**
* Get generator metadata (self-contained)
* @returns {Object} Generator metadata
*/
static getMetadata() {
return {
name: 'html',
description: 'Professional, responsive HTML documentation generator',
version: '1.9.9',
packageName: '@confytome/html',
cliCommand: 'confytome-html',
inputs: ['api-spec.json'],
outputs: ['api-docs.html'],
features: ['responsive-design', 'professional-styling', 'modern-navigation']
};
}
/**
* Validate generator prerequisites (extends base validation)
* @param {Object} options - Validation options
* @returns {Promise<ValidationResult>} Validation result
*/
async validate(options = {}) {
const baseValidation = await super.validate(options);
// HTML generator doesn't require additional templates - everything is inline
this.log('HTML generator validation complete');
return baseValidation;
}
/**
* Initialize the generator (extends base initialization)
* @param {Object} options - Initialization options
* @returns {Promise<ValidationResult>} Initialization result
*/
async initialize(options = {}) {
const baseInit = await super.initialize(options);
// Add HTML-specific initialization here if needed
this.log('HTML generator initialized');
return baseInit;
}
/**
* Generate HTML documentation
* @param {Object} options - Generation options
* @returns {Promise<GenerationResult>} Generation result
*/
async generate(_options = {}) {
this.log('🎨 Generating HTML documentation...');
try {
// Load and parse OpenAPI spec using base class method
const spec = this.loadOpenAPISpec();
// Configure processor options
const processorOptions = {
excludeBrand: this.options.excludeBrand,
version: this.getInfo().version,
baseUrl: this.getBaseUrl(spec.servers),
tagOrder: this.options.tagOrder || spec.tags?.map(tag => tag.name) || []
};
// Process OpenAPI spec into template data
this.processor = new OpenApiProcessor(processorOptions);
const data = this.processor.process(spec);
// Generate HTML content
const html = this.generateHtmlContent(data);
// Write output using base class method
const result = this.writeOutputFile('api-docs.html', html, 'Professional HTML documentation created');
// Add additional stats
if (result.success) {
result.stats = {
...result.stats,
endpoints: data.endpoints?.length || 0,
resources: data.resources?.length || 0,
schemas: data.schemas?.models?.length || 0
};
}
return result;
} catch (error) {
this.log(`HTML generation failed: ${error.message}`, 'error');
return {
success: false,
outputPath: null,
size: 0,
stats: { error: error.message }
};
}
}
/**
* Generate HTML content
* @param {Object} data - Processed OpenAPI data
* @returns {string} Complete HTML document
*/
generateHtmlContent(data) {
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${data.title} v${data.version} - API Documentation</title>
<style>
${this.getCSSStyles()}
</style>
</head>
<body>
<div class="container">
<header class="header">
<h1>${data.title}</h1>
<p class="version">Version ${data.version}</p>
${data.description ? `<p class="description">${data.description}</p>` : ''}
</header>
${data.baseUrl ? `<div class="base-url">
<h2>Base URL</h2>
<code>${data.baseUrl}</code>
</div>` : ''}
<div class="content">
${this.generateEndpointsSection(data.endpoints)}
${this.generateSchemasSection(data.schemas)}
</div>
<hr style="margin: 3rem 0; border: none; border-top: 1px solid #dee2e6;">
<p style="text-align: center; color: #6c757d;">
${this.generateBranding('html')}
</p>
</div>
<script>
${this.getJavaScript()}
</script>
</body>
</html>`;
}
/**
* Generate endpoints documentation section
*/
generateEndpointsSection(endpoints) {
if (!endpoints || !endpoints.length) {
return '<div class="section"><h2>Endpoints</h2><p>No endpoints defined.</p></div>';
}
const endpointsHtml = endpoints.map(endpoint => `
<div class="endpoint">
<div class="endpoint-header">
<span class="method method-${endpoint.method.toLowerCase()}">${endpoint.method}</span>
<code class="path">${endpoint.path}</code>
</div>
${endpoint.summary ? `<h4>${endpoint.summary}</h4>` : ''}
${endpoint.description ? `<p>${endpoint.description}</p>` : ''}
${endpoint.parameters && endpoint.parameters.length > 0 ? `
<div class="parameters">
<h5>Parameters</h5>
<table>
<thead>
<tr><th>Name</th><th>Type</th><th>In</th><th>Required</th><th>Description</th></tr>
</thead>
<tbody>
${endpoint.parameters.map(param => `
<tr>
<td><code>${param.name}</code></td>
<td>${param.type}</td>
<td>${param.in}</td>
<td>${param.required ? 'Yes' : 'No'}</td>
<td>${param.description}</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
` : ''}
${endpoint.responses && endpoint.responses.length > 0 ? `
<div class="responses">
<h5>Responses</h5>
<table>
<thead>
<tr><th>Status</th><th>Description</th></tr>
</thead>
<tbody>
${endpoint.responses.map(response => `
<tr>
<td><span class="status-code status-${response.statusCode[0]}xx">${response.statusCode}</span></td>
<td>${response.description}</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
` : ''}
</div>
`).join('');
return `<div class="section">
<h2>Endpoints</h2>
${endpointsHtml}
</div>`;
}
/**
* Generate schemas documentation section
*/
generateSchemasSection(schemas) {
if (!schemas || !schemas.models || !schemas.models.length) {
return '<div class="section"><h2>Schemas</h2><p>No schemas defined.</p></div>';
}
const schemasHtml = schemas.models.map(schema => `
<div class="schema">
<h3>${schema.name}</h3>
${schema.description ? `<p>${schema.description}</p>` : ''}
${schema.properties && schema.properties.length > 0 ? `
<table>
<thead>
<tr><th>Property</th><th>Type</th><th>Description</th></tr>
</thead>
<tbody>
${schema.properties.map(prop => `
<tr>
<td>
<code>${prop.name}</code>
${schema.required.includes(prop.name) ? ' <span class="required">*</span>' : ''}
</td>
<td>${prop.type}${prop.format ? ` (${prop.format})` : ''}</td>
<td>${prop.description}</td>
</tr>
`).join('')}
</tbody>
</table>
` : ''}
</div>
`).join('');
return `<div class="section">
<h2>Schemas</h2>
${schemasHtml}
</div>`;
}
/**
* Get CSS styles for the HTML document
*/
getCSSStyles() {
return `
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
line-height: 1.6;
color: #333;
background-color: #f8f9fa;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
}
.header {
background: white;
padding: 2rem;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
margin-bottom: 2rem;
}
.header h1 {
color: #2c3e50;
margin-bottom: 0.5rem;
}
.version {
color: #7f8c8d;
font-size: 1.1rem;
margin-bottom: 1rem;
}
.description {
color: #555;
}
.base-url {
background: white;
padding: 1.5rem;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
margin-bottom: 2rem;
}
.base-url h2 {
margin-bottom: 1rem;
color: #2c3e50;
}
.base-url code {
background: #f1f2f6;
padding: 0.5rem 1rem;
border-radius: 4px;
font-family: 'Monaco', 'Consolas', monospace;
font-size: 1.1rem;
}
.section {
background: white;
padding: 2rem;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
margin-bottom: 2rem;
}
.section h2 {
color: #2c3e50;
margin-bottom: 1.5rem;
padding-bottom: 0.5rem;
border-bottom: 2px solid #3498db;
}
.endpoint {
border: 1px solid #ddd;
border-radius: 6px;
margin-bottom: 1.5rem;
overflow: hidden;
}
.endpoint-header {
background: #f8f9fa;
padding: 1rem;
display: flex;
align-items: center;
gap: 1rem;
}
.method {
padding: 0.25rem 0.75rem;
border-radius: 4px;
font-weight: bold;
text-transform: uppercase;
font-size: 0.8rem;
min-width: 60px;
text-align: center;
}
.method-get { background: #d4edda; color: #155724; }
.method-post { background: #cce7ff; color: #004085; }
.method-put { background: #fff3cd; color: #856404; }
.method-delete { background: #f8d7da; color: #721c24; }
.method-patch { background: #e2e3e5; color: #383d41; }
.path {
font-family: 'Monaco', 'Consolas', monospace;
font-size: 1.1rem;
background: white;
padding: 0.5rem;
border-radius: 4px;
flex-grow: 1;
}
.endpoint > div:not(.endpoint-header) {
padding: 1rem;
}
.endpoint h4 {
color: #2c3e50;
margin-bottom: 0.5rem;
}
.parameters, .responses {
margin-top: 1rem;
}
.parameters h5, .responses h5 {
color: #34495e;
margin-bottom: 0.75rem;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 0.5rem;
}
th, td {
text-align: left;
padding: 0.75rem;
border-bottom: 1px solid #ddd;
}
th {
background: #f8f9fa;
font-weight: 600;
color: #2c3e50;
}
.status-code {
padding: 0.25rem 0.5rem;
border-radius: 3px;
font-weight: bold;
font-size: 0.85rem;
}
.status-2xx { background: #d4edda; color: #155724; }
.status-3xx { background: #cce7ff; color: #004085; }
.status-4xx { background: #fff3cd; color: #856404; }
.status-5xx { background: #f8d7da; color: #721c24; }
.schema {
border: 1px solid #ddd;
border-radius: 6px;
padding: 1.5rem;
margin-bottom: 1.5rem;
}
.schema h3 {
color: #2c3e50;
margin-bottom: 1rem;
}
.required {
color: #e74c3c;
font-weight: bold;
}
@media (max-width: 768px) {
.container {
padding: 1rem;
}
.endpoint-header {
flex-direction: column;
align-items: flex-start;
}
.path {
word-break: break-all;
}
}
`;
}
/**
* Get JavaScript for interactive features
*/
getJavaScript() {
return `
// Add any interactive features here
console.log('HTML documentation loaded');
`;
}
}