@raven-js/glean
Version:
Glean documentation gold from your codebase - JSDoc parsing, validation, and beautiful doc generation
336 lines (314 loc) ⢠9.54 kB
JavaScript
/**
* @author Anonyfox <max@anonyfox.com>
* @license MIT
* @see {@link https://github.com/Anonyfox/ravenjs}
* @see {@link https://ravenjs.dev}
* @see {@link https://anonyfox.com}
*/
/**
* Module overview template for /modules/{moduleName}/ route
*
* Renders individual module documentation with optimized content flow:
* module description as lead text, API reference first, then README,
* and attribution at the bottom. Uses Bootstrap 5 components.
* Follows WEBAPP.md specification for module overview presentation.
*/
import { html, markdownToHTML, safeHtml } from "@raven-js/beak";
import { createModuleAttribution } from "../../extract/models/attribution.js";
import {
applySyntaxHighlighting,
attributionBar,
contentSection,
pageHeader,
seeAlsoLinks,
} from "../components/index.js";
import { baseTemplate } from "./base.js";
/**
* Apply asset path rewriting to HTML content if asset registry is available
* @param {string} html - HTML content to process
* @param {import('../../assets/registry.js').AssetRegistry} [assetRegistry] - Asset registry
* @returns {string} HTML with rewritten asset paths
*/
function applyAssetPathRewriting(html, assetRegistry) {
if (!assetRegistry || typeof html !== "string") {
return html;
}
return assetRegistry.rewriteImagePaths(html);
}
/**
* Properly pluralize entity type names
* @param {string} type - Entity type
* @param {number} count - Entity count
* @returns {string} Properly pluralized type name
*/
function pluralizeType(type, count) {
if (count === 1) return type;
// Handle special cases
/** @type {{[key: string]: string}} */
const pluralMap = {
class: "classes",
function: "functions",
variable: "variables",
typedef: "typedefs",
callback: "callbacks",
};
return pluralMap[type] || `${type}s`;
}
/**
* Get Bootstrap variant class for entity type badge
* @param {string} entityType - The entity type (function, class, etc.)
* @returns {string} Bootstrap variant class
*/
function getTypeVariant(entityType) {
/** @type {Record<string, string>} */
const typeVariantMap = {
function: "primary",
class: "success",
typedef: "info",
interface: "warning",
enum: "secondary",
constant: "dark",
variable: "light",
};
return typeVariantMap[entityType] || "secondary";
}
/**
* Generate module overview HTML page with entity listings and module documentation.
*
* @param {any} data - Module overview data from extractor
* @param {any} assetRegistry - Asset registry for path rewriting
* @returns {string} Complete HTML page
*
* @example
* // Basic module overview
* moduleOverviewTemplate({
* module: { name: 'utils', fullName: 'my-package/utils' },
* organizedEntities: { function: [...] },
* stats: { totalEntities: 5 },
* packageName: 'my-package'
* });
*/
export function moduleOverviewTemplate(data, assetRegistry, options = {}) {
const { urlBuilder } = /** @type {any} */ (options);
const { module, organizedEntities, stats, packageName, hasEntities } =
/** @type {any} */ (data);
// Process README content through beak markdown processor and apply syntax highlighting
const readmeHTML = module.hasReadme
? applyAssetPathRewriting(
applySyntaxHighlighting(markdownToHTML(module.readme)),
assetRegistry,
)
: "";
// Generate breadcrumbs
const breadcrumbs = [
{
href: urlBuilder ? /** @type {any} */ (urlBuilder).homeUrl() : "/",
text: `š¦ ${packageName}`,
},
{
href: urlBuilder
? /** @type {any} */ (urlBuilder).modulesUrl()
: "/modules/",
text: "Modules",
},
{ text: module.name, active: true },
];
// Generate header badges
const badges = [
{
text: `${stats.totalEntities} Entit${stats.totalEntities !== 1 ? "ies" : "y"}`,
variant: "secondary",
},
];
if (module.isDefault) {
badges.unshift({ text: "default", variant: "primary" });
}
// Create module-level attribution context
let moduleAttributionContext = null;
try {
const moduleEntities = /** @type {any} */ (data).moduleEntities || [];
const packageMetadata = /** @type {any} */ (data).packageMetadata;
if (moduleEntities.length > 0) {
moduleAttributionContext = createModuleAttribution(
moduleEntities,
packageMetadata,
/** @type {any} */ (data).allModules, // Pass all modules for re-export tracing
);
}
} catch (_error) {
// Silently fail if attribution creation fails
moduleAttributionContext = null;
}
// Generate main content
const content = html`
${pageHeader({
title: module.name,
breadcrumbs,
badges,
})}
<!-- Module Description -->
${
module.description
? html`
<div class="lead text-muted mb-4">
${markdownToHTML(module.description)}
</div>
`
: ""
}
<!-- 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="d-flex align-items-center gap-3">
<div class="flex-grow-1">
<div class="input-group">
<input type="text" class="form-control font-monospace" value="import { /* ... */ } from '${module.fullName}';" readonly id="import-${module.name}">
<button class="btn btn-outline-primary" type="button" onclick="copyImportStatement('import-${module.name}')" title="Copy import statement">
š Copy
</button>
</div>
</div>
${
hasEntities
? html`
<div class="text-center">
<div class="fw-bold text-primary">${stats.totalEntities}</div>
<small class="text-muted">Export${stats.totalEntities !== 1 ? "s" : ""}</small>
</div>
`
: ""
}
</div>
</div>
</div>
<!-- API Reference Section -->
${
hasEntities
? contentSection({
title: "š§ API Reference",
content: Object.entries(organizedEntities)
.map(
/** @param {[string, Array<any>]} entry */
([type, entities]) => {
return html`
<div class="mb-4">
<h4 class="h6 fw-bold text-uppercase text-muted mb-3">
${pluralizeType(type, entities.length).toUpperCase()} (${entities.length})
</h4>
<div class="list-group list-group-flush">
${entities.map(
(entity) => html`
<div class="list-group-item d-flex justify-content-between align-items-start">
<div class="flex-grow-1 me-3">
<div class="d-flex align-items-center mb-2">
<h5 class="mb-0 me-2">
<a href="${safeHtml`${entity.link}`}" class="text-decoration-none">${safeHtml`${entity.name}`}</a>
</h5>
<span class="badge bg-${getTypeVariant(entity.entityType || type)} me-2">${safeHtml`${entity.entityType || type}`}</span>
${entity.isDeprecated ? html`<span class="badge bg-warning">deprecated</span>` : ""}
${entity.hasExamples ? html`<span class="badge bg-success">examples</span>` : ""}
</div>
${
entity.description
? html`
<div class="text-muted mb-2">
${markdownToHTML(entity.description)}
</div>
`
: html`
<div class="text-muted fst-italic mb-2">No description available</div>
`
}
</div>
<div class="flex-shrink-0">
<a href="${safeHtml`${entity.link}`}" class="btn btn-outline-primary btn-sm">
š View
</a>
</div>
</div>
`,
)}
</div>
</div>
`;
},
)
.join(""),
})
: html`
<div class="text-center py-5">
<div class="display-1 mb-3">š</div>
<h3 class="text-muted mb-3">No Public Entities</h3>
<p class="text-muted">
This module doesn't export any public APIs.
${module.hasReadme ? "Check the documentation above for usage information." : ""}
</p>
</div>
`
}
<!-- README Section -->
${
module.hasReadme
? contentSection({
title: "š Documentation",
content: readmeHTML,
})
: ""
}
<!-- Module Attribution -->
${
moduleAttributionContext?.hasAttribution
? html`
<div class="card border-info mb-4">
<div class="card-body py-2">
${attributionBar(moduleAttributionContext)}
${seeAlsoLinks(moduleAttributionContext)}
</div>
</div>
`
: ""
}
`;
// Return complete HTML page using base template
return baseTemplate({
title: module.name,
description: `Documentation for ${module.fullName} module in ${packageName}${module.hasReadme ? "" : ` - ${stats.totalEntities} entities`}`,
packageName,
content,
navigation: {
current: "modules",
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>
`
: "",
},
seo: {
url: "", // Will be filled by route handler
},
packageMetadata: /** @type {any} */ (data).packageMetadata,
generationTimestamp: /** @type {any} */ (data).generationTimestamp,
urlBuilder,
});
}