UNPKG

@raven-js/glean

Version:

Glean documentation gold from your codebase - JSDoc parsing, validation, and beautiful doc generation

626 lines (579 loc) â€ĸ 17.6 kB
/** * @author Anonyfox <max@anonyfox.com> * @license MIT * @see {@link https://github.com/Anonyfox/ravenjs} * @see {@link https://ravenjs.dev} * @see {@link https://anonyfox.com} */ /** * Entity page template for /modules/{moduleName}/{entityName}/ route. * * Renders individual entity documentation with comprehensive JSDoc processing, * code examples, cross-references, and navigation context using Bootstrap 5. */ import { html, markdownToHTML, safeHtml } from "@raven-js/beak"; import { createEntityAttribution } from "../../extract/models/attribution.js"; import { attributionBar, codeBlock, contentSection, deprecationAlert, entityCard, pageHeader, seeAlsoLinks, tableSection, } from "../components/index.js"; import { baseTemplate } from "./base.js"; /** * Generate entity documentation HTML page with comprehensive JSDoc sections. * * @param {any} data - Entity page data from extractor with comprehensive structure * @returns {string} Complete HTML page * * @example * // Basic function entity page * entityPageTemplate({ * entity: { name: 'myFunction', type: 'function', description: '...' }, * documentation: { parameters: [...], returns: {...}, examples: [...] }, * packageName: 'my-package', * moduleName: 'utils' * }); */ export function entityPageTemplate(data, options = {}) { const { urlBuilder } = /** @type {any} */ (options); const { entity, documentation, relatedEntities, packageName, moduleName, hasParameters, hasProperties, hasMethods, hasReturns, hasExamples, hasRelatedEntities, hasTypeInfo, isDeprecated, } = data; // Type casts for object property access /** @type {any} */ const ent = entity; /** @type {any} */ const docs = documentation; // Create attribution context for entity (defensive) let attributionContext = null; try { const entityInstance = /** @type {any} */ (data).entityInstance; if ( entityInstance && typeof (/** @type {any} */ (entityInstance).getJSDocTagsByType) === "function" ) { attributionContext = createEntityAttribution( /** @type {any} */ (entityInstance), /** @type {any} */ (data).packageMetadata, /** @type {any} */ (data).allModules, // Pass all modules for re-export tracing ); } } catch (_error) { // Silently fail if attribution creation fails (e.g., in tests with mock entities) attributionContext = null; } // Generate entity type badge color const getTypeBadgeClass = (/** @type {string} */ type) => { /** @type {Record<string, string>} */ const typeColors = { function: "bg-primary", class: "bg-success", typedef: "bg-info", interface: "bg-warning text-dark", enum: "bg-secondary", constant: "bg-dark", variable: "bg-light text-dark", }; return typeColors[type] || "bg-secondary"; }; // Helper function to link type names to entity pages if they exist const linkTypeIfEntity = ( /** @type {string} */ typeString, /** @type {string} */ colorClass = "text-secondary", ) => { if (!typeString) return html`<span class="text-muted">any</span>`; // Check all available entities (same module, references, and referenced by) /** @type {{sameModule?: any[], similar?: any[], references?: any[], referencedBy?: any[]}} */ const typedRelatedEntities = relatedEntities; const allEntities = [ ...(typedRelatedEntities.sameModule || []), ...(typedRelatedEntities.references || []), ...(typedRelatedEntities.referencedBy || []), ]; // If the type matches the current entity, link to current page if (typeString === entity.name) { const entityUrl = entity.importPath && urlBuilder ? /** @type {any} */ (urlBuilder).entityUrl( entity.importPath.split("/").pop(), entity.name, ) : entity.importPath ? `/modules/${entity.importPath.split("/").pop()}/${entity.name}/` : "#"; return html`<a href="${entityUrl}" class="text-decoration-none"><code class="${colorClass}">${safeHtml`${typeString}`}</code></a>`; } /** @type {any} */ const matchingEntity = allEntities.find( /** @param {any} ent */ (ent) => ent.name === typeString, ); if (matchingEntity) { return html`<a href="${safeHtml`${matchingEntity.link}`}" class="text-decoration-none"><code class="${colorClass}">${safeHtml`${typeString}`}</code></a>`; } // No matching entity, return as plain text return html`<code class="${colorClass}">${safeHtml`${typeString}`}</code>`; }; // Generate breadcrumbs const breadcrumbs = [ { href: urlBuilder ? /** @type {any} */ (urlBuilder).homeUrl() : "/", text: `đŸ“Ļ ${packageName}`, }, { href: urlBuilder ? /** @type {any} */ (urlBuilder).modulesUrl() : "/modules/", text: "Modules", }, { href: urlBuilder ? /** @type {any} */ (urlBuilder).moduleUrl(moduleName) : `/modules/${moduleName}/`, text: moduleName, }, { text: entity.name, active: true }, ]; // Generate header badges const badges = [ { text: ent.type, variant: getTypeBadgeClass(ent.type).replace("bg-", "") }, ]; if (ent.isDefault) { badges.push({ text: "default module", variant: "primary" }); } // Generate metadata description (removed file location to save space) const metadataItems = []; if (docs.since) { metadataItems.push(`📅 Since ${docs.since}`); } if (docs.author.length > 0) { metadataItems.push(`👤 ${docs.author.join(", ")}`); } // Generate main content const content = html` ${pageHeader({ title: ent.name, subtitle: ent.description, breadcrumbs, badges, description: metadataItems.length > 0 ? metadataItems.join(" â€ĸ ") : undefined, })} ${ isDeprecated ? deprecationAlert({ reason: docs.deprecated.reason, since: docs.deprecated.since, }) : "" } <!-- Import Statement --> <div class="card border-primary mb-4"> <div class="card-header bg-primary text-white"> <h5 class="mb-0">đŸ“Ļ Import</h5> </div> <div class="card-body"> <div class="input-group"> <input type="text" class="form-control font-monospace" value="${safeHtml`${ent.importStatement}`}" readonly id="import-${ent.name}"> <button class="btn btn-outline-primary" type="button" onclick="copyImportStatement('import-${ent.name}')" title="Copy import statement"> 📋 Copy </button> </div> </div> </div> <!-- Main Documentation --> <div class="mb-4"> <!-- TypeScript Signature --> ${ hasTypeInfo && docs.typeInfo.signature ? contentSection({ title: "Type Signature", icon: "📝", content: codeBlock({ code: docs.typeInfo.signature, language: "typescript", showCopy: false, }), }) : "" } <!-- Parameters --> ${ hasParameters ? contentSection({ title: "Parameters", icon: "âš™ī¸", noPadding: true, content: tableSection({ headers: ["Name", "Type", "Description", "Default"], rows: docs.parameters.map( /** @param {any} param */ (param) => [ html`<code class="text-primary">${safeHtml`${param.name}`}</code>${param.isOptional ? html`<span class="text-muted">?</span>` : ""}`, linkTypeIfEntity(param.type), param.description ? markdownToHTML(param.description) : html`<em class="text-muted">No description</em>`, param.defaultValue ? html`<code class="text-success">${safeHtml`${param.defaultValue}`}</code>` : param.isOptional ? html`<span class="text-muted">undefined</span>` : html`<span class="text-muted">required</span>`, ], ), }), }) : "" } <!-- Properties --> ${ hasProperties ? contentSection({ title: "Properties", icon: "📝", noPadding: true, content: tableSection({ headers: ["Property", "Type", "Modifiers"], rows: docs.properties .sort( /** @param {{name: string}} a @param {{name: string}} b */ ( a, b, ) => a.name.localeCompare(b.name), ) .map( /** @param {any} property */ (property) => [ // Property name and description html`<div class="mb-1"> <code class="text-primary">${safeHtml`${property.name}`}</code>${property.isOptional ? html`<span class="text-muted">?</span>` : ""} </div> ${ property.description ? html`<div class="small text-muted mt-1">${markdownToHTML(property.description)}</div>` : html`<div class="small text-muted fst-italic mt-1">No description</div>` }`, // Type column linkTypeIfEntity(property.type), // Handle both typedef properties (required/optional) and class properties (static/instance/private) html`<div class="d-flex gap-1 flex-wrap"> ${ property.isOptional ? html`<span class="badge bg-secondary">optional</span>` : Object.hasOwn(property, "isOptional") ? html`<span class="badge bg-primary">required</span>` : "" } ${ property.isStatic ? html`<span class="badge bg-info">static</span>` : "" } ${ property.isPrivate ? html`<span class="badge bg-dark">private</span>` : "" } ${ property.isInstance ? html`<span class="badge bg-success">instance</span>` : "" } ${ property.defaultValue && property.defaultValue !== null ? html`<span class="badge bg-warning text-dark">has default</span>` : "" } </div>`, ], ), }), }) : "" } <!-- Methods --> ${ hasMethods ? contentSection({ title: "Methods", icon: "🔧", noPadding: true, content: tableSection({ headers: ["Method", "Parameters", "Returns", "Modifiers"], rows: docs.methods .sort( /** @param {{name: string}} a @param {{name: string}} b */ ( a, b, ) => a.name.localeCompare(b.name), ) .map( /** @param {any} method */ (method) => [ // Method name and description html`<div class="mb-1"> <code class="text-primary">${safeHtml`${method.name}`}</code> <span class="badge bg-${method.type === "constructor" ? "success" : "primary"} ms-2">${safeHtml`${method.type}`}</span> ${method.isDeprecated ? html`<span class="badge bg-warning ms-1">deprecated</span>` : ""} </div> ${ method.description ? html`<div class="small text-muted mt-1">${markdownToHTML(method.description)}</div>` : html`<div class="small text-muted fst-italic mt-1">No description</div>` }`, // Parameters column - each on its own line method.parameters && method.parameters.length > 0 ? html`<div class="small"> ${method.parameters .map( /** @param {any} param @param {number} index */ ( param, index, ) => html` <div class="mb-2"> ${index > 0 ? html`<hr class="my-2"/>` : ""} <code class="text-info">${safeHtml`${param.name}`}${param.isOptional ? "?" : ""}</code>: ${linkTypeIfEntity(param.type, "text-secondary")} ${param.description ? html`<br><span class="text-muted">${safeHtml`${param.description}`}</span>` : ""} </div> `, ) .join("")} </div>` : html`<span class="text-muted fst-italic">None</span>`, // Returns column linkTypeIfEntity(method.returnType), // Method modifiers badges html`<div class="d-flex gap-1 flex-wrap"> ${ method.isStatic ? html`<span class="badge bg-info">static</span>` : "" } ${ method.isPrivate ? html`<span class="badge bg-dark">private</span>` : "" } ${ method.isAsync ? html`<span class="badge bg-warning text-dark">async</span>` : "" } ${ method.isGenerator ? html`<span class="badge bg-secondary">generator</span>` : "" } </div>`, ], ), }), }) : "" } <!-- Returns --> ${ hasReturns ? contentSection({ title: "Returns", icon: "â†Šī¸", content: html` ${ docs.returns.type ? html` <div class="mb-2"> <strong>Type:</strong> ${linkTypeIfEntity(docs.returns.type)} </div> ` : "" } ${ docs.returns.description ? html` <div class="mb-0">${markdownToHTML(docs.returns.description)}</div> ` : "" } `, }) : "" } <!-- Exceptions/Throws --> ${ docs.throws.length > 0 ? contentSection({ title: "Exceptions", icon: "🚨", noPadding: true, content: tableSection({ headers: ["Type", "Description"], rows: docs.throws.map( /** @param {any} throwsInfo */ (throwsInfo) => [ linkTypeIfEntity(throwsInfo.type, "text-danger"), throwsInfo.description ? markdownToHTML(throwsInfo.description) : html`<em class="text-muted">No description</em>`, ], ), }), }) : "" } <!-- Examples --> ${ hasExamples ? contentSection({ title: "Examples", icon: "💡", noPadding: true, content: html` ${docs.examples.map( /** @param {any} example */ (example) => html` <div class="border-bottom p-4"> ${codeBlock({ code: example.code, language: example.language, title: example.title, })} </div> `, )} `, }) : "" } <!-- Attribution --> ${ attributionContext?.hasAttribution ? html` <div class="card border-info mb-4"> <div class="card-body py-2"> ${attributionBar(attributionContext)} ${seeAlsoLinks(attributionContext)} </div> </div> ` : "" } <!-- Related Entities --> ${ hasRelatedEntities ? contentSection({ title: "Related APIs", icon: "🔄", content: html` ${ relatedEntities.sameModule.length > 0 ? html` <div class="mb-4"> <h6 class="fw-bold mb-3">Same Module (${moduleName})</h6> <div class="row g-3"> ${relatedEntities.sameModule.map( /** @param {any} related */ (related) => html` <div class="col-md-6"> ${entityCard({ name: related.name, type: related.type, description: related.description, link: related.link, showFooter: false, })} </div> `, )} </div> </div> ` : "" } ${ relatedEntities.similar.length > 0 ? html` <div class="mb-0"> <h6 class="fw-bold mb-3">Similar APIs (${ent.type})</h6> <div class="row g-3"> ${relatedEntities.similar.map( /** @param {any} similar */ (similar) => html` <div class="col-md-6"> ${entityCard({ name: similar.name, type: ent.type, description: similar.description, link: similar.link, badges: [ { text: similar.moduleName, variant: "secondary" }, ], showFooter: false, })} </div> `, )} </div> </div> ` : "" } `, }) : "" } </div> `; // Generate sidebar navigation (matching module-overview.js exactly) const sidebar = /** @type {any} */ (data).navigation.allModules.length > 1 ? html` <h6 class="fw-bold mb-3">Module Navigation</h6> <ul class="nav nav-pills flex-column"> ${ /** @type {any} */ (data).navigation.allModules.map( /** @param {any} navModule */ (navModule) => html` <li class="nav-item"> <a href="${navModule.link}" class="nav-link ${navModule.isCurrent ? "active" : ""}" > ${navModule.isDefault ? "đŸ“Ļ " : "📄 "}${navModule.name} </a> </li> `, ) } </ul> ` : ""; // Return complete HTML page using base template return baseTemplate({ title: `${ent.name} - ${moduleName}`, description: `API documentation for ${ent.name} ${ent.type} in ${packageName}/${moduleName}${ent.description ? ` - ${ent.description}` : ""}`, packageName, content, navigation: { current: "modules", sidebar, }, seo: { url: "", // Will be filled by route handler }, packageMetadata: /** @type {any} */ (data).packageMetadata, generationTimestamp: /** @type {any} */ (data).generationTimestamp, urlBuilder, }); }