@graphql-markdown/core
Version:
GraphQL-Markdown core package for generating Markdown documentation from a GraphQL schema.
773 lines (772 loc) • 33.2 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getRenderer = exports.Renderer = exports.getApiGroupFolder = exports.API_GROUPS = void 0;
const node_path_1 = require("node:path");
const graphql_1 = require("@graphql-markdown/graphql");
const utils_1 = require("@graphql-markdown/utils");
const logger_1 = require("@graphql-markdown/logger");
const config_1 = require("./config");
const validation_1 = require("./directives/validation");
const event_emitter_1 = require("./event-emitter");
const events_1 = require("./events");
/**
* Constant representing the string literal for deprecated entities.
* Used for grouping deprecated schema entities when the deprecated option is set to "group".
*
* @example
* ```typescript
* // When using the deprecated grouping option
* if (isDeprecated(type)) {
* const groupName = DEPRECATED;
* // Handle deprecated type
* }
* ```
*/
const DEPRECATED = "deprecated";
/**
* CSS class names used for styling different categories in the generated documentation.
* These are applied to the generated index metafiles for different section types.
*
* @example
* ```typescript
* // When generating API section metafiles
* await generateIndexMetafile(dirPath, typeCat, {
* styleClass: CATEGORY_STYLE_CLASS.API
* });
* ```
*/
var CATEGORY_STYLE_CLASS;
(function (CATEGORY_STYLE_CLASS) {
/**
* CSS class applied to API sections (operations)
*/
CATEGORY_STYLE_CLASS["API"] = "graphql-markdown-api-section";
/**
* CSS class applied to deprecated entity sections
*/
CATEGORY_STYLE_CLASS["DEPRECATED"] = "graphql-markdown-deprecated-section";
})(CATEGORY_STYLE_CLASS || (CATEGORY_STYLE_CLASS = {}));
/**
* Enum defining sidebar position values for ordering categories in the documentation sidebar.
* Used to control the relative position of categories in the navigation.
*
* @example
* ```typescript
* // Position deprecated entities at the end of the sidebar
* await generateIndexMetafile(dirPath, DEPRECATED, {
* sidebarPosition: SidebarPosition.LAST
* });
* ```
*/
var SidebarPosition;
(function (SidebarPosition) {
/**
* Position at the beginning of the sidebar
*/
SidebarPosition[SidebarPosition["FIRST"] = 1] = "FIRST";
/**
* Position at the end of the sidebar
*/
SidebarPosition[SidebarPosition["LAST"] = 999] = "LAST";
})(SidebarPosition || (SidebarPosition = {}));
/**
* Default group names for API types and non-API types.
* This constant provides the base folder structure for organizing GraphQL schema entities.
* Can be overridden via ApiGroupOverrideType in configuration.
*
* @property operations Folder name for GraphQL operations (queries, mutations, subscriptions)
* @property types Folder name for GraphQL type definitions
*
* @example
* ```typescript
* // Default structure
* const defaultGroups = API_GROUPS;
* // { operations: "operations", types: "types" }
*
* // With custom override
* const customGroups = { ...API_GROUPS, operations: "queries-and-mutations" };
* ```
*
* @see {@link getApiGroupFolder} For usage with type categorization
*/
exports.API_GROUPS = {
operations: "operations",
types: "types",
};
/**
* Determines the appropriate folder for a GraphQL schema entity based on its type.
*
* @param type - The GraphQL schema entity to categorize
* @param groups - Optional custom group naming configuration
* @returns The folder name where the entity should be placed
*
* @example
* ```typescript
* // With default groups
* const folder = getApiGroupFolder(queryType); // Returns "operations"
*
* // With custom groups
* const folder = getApiGroupFolder(objectType, { operations: "queries" }); // Returns appropriate folder
* ```
*/
const getApiGroupFolder = (type, groups) => {
let folderNames = exports.API_GROUPS;
if ((0, validation_1.isGroupsObject)(groups)) {
folderNames = { ...exports.API_GROUPS, ...groups };
}
return (0, graphql_1.isApiType)(type) ? folderNames.operations : folderNames.types;
};
exports.getApiGroupFolder = getApiGroupFolder;
/**
* Strips numeric prefix from a folder name if categorySort is enabled.
* Converts folder names like "01-query" back to "query" for category identification.
*
* This is needed when extracting category names from file paths that were created
* with categorySort enabled. The regex matches leading two-digit numbers
* followed by a hyphen.
*
* @param folderName - The folder name to strip (e.g., "01-query", "02-mutations")
* @returns The folder name without prefix (e.g., "query", "mutations")
*
* @example
* ```typescript
* stripNumericPrefix("01-query"); // Returns "query"
* stripNumericPrefix("02-mutations"); // Returns "mutations"
* stripNumericPrefix("objects"); // Returns "objects" (no prefix to strip)
* ```
*/
const stripNumericPrefix = (folderName) => {
return folderName.replace(/^\d{2}-/, "");
};
/**
* Type guard function that checks if the provided options include a specific hierarchy configuration.
*
* @param options - The renderer options to check
* @param hierarchy - The hierarchy type to check for
* @returns True if the options contain the specified hierarchy configuration
*
* @example
* ```typescript
* if (isHierarchy(options, TypeHierarchy.FLAT)) {
* // Handle flat hierarchy structure
* }
* ```
*/
const isHierarchy = (options, hierarchy) => {
return options?.hierarchy?.[hierarchy] !== undefined;
};
/**
* Default natural sorting function for categories.
* Sorts categories alphabetically using localeCompare.
*
* @param a - First category name
* @param b - Second category name
* @returns Comparison result for sorting
*/
const naturalSort = (a, b) => {
return a.localeCompare(b);
};
/**
* Manages category positions for the sidebar.
* Supports two modes:
* 1. Pre-registration: categories are registered upfront, positions computed once
* 2. On-demand: positions are computed as categories are encountered
*
*/
class CategoryPositionManager {
categories = new Set();
positionCache = new Map();
positionsComputed = false;
sortFn;
basePosition;
/**
* Creates a new CategoryPositionManager.
*
* @param sortFn - Function to sort categories (defaults to natural/alphabetical)
* @param basePosition - Starting position for categories (defaults to 1)
*/
constructor(sortFn, basePosition = 1) {
this.sortFn = sortFn === "natural" || !sortFn ? naturalSort : sortFn;
this.basePosition = basePosition;
}
/**
* Pre-registers a batch of category names.
* This should be called before rendering to ensure consistent positioning.
*
* @param categoryNames - Array of category names to register
*/
registerCategories(categoryNames) {
for (const name of categoryNames) {
this.categories.add(name);
}
}
/**
* Computes positions for all registered categories.
* This is called either manually after registration or automatically on first getPosition call.
*/
computePositions() {
if (this.positionsComputed) {
return;
}
const sorted = Array.from(this.categories).sort(this.sortFn);
for (let index = 0; index < sorted.length; index++) {
this.positionCache.set(sorted[index], this.basePosition + index);
}
this.positionsComputed = true;
}
/**
* Gets the assigned position for a category.
* If category positions haven't been computed yet, computes them first.
*
* @param category - The category name
* @returns The position assigned to this category or basePosition if not found
*/
getPosition(category) {
// Ensure positions are computed first (uses pre-registered categories)
if (!this.positionsComputed) {
this.computePositions();
}
// Return cached position or base position if not found
// NOTE: We don't dynamically add categories here to avoid breaking
// pre-computed positions. This ensures consistent positioning even if
// getPosition is called for categories that weren't pre-registered.
return this.positionCache.get(category) ?? this.basePosition;
}
/**
* Check if a category was pre-registered without triggering recomputation.
* Used to determine hierarchy level (root vs nested) without side effects.
*
* @param category - Category name to check
* @returns true if category was pre-registered
*/
isRegistered(category) {
return this.categories.has(category);
}
}
/**
* Core renderer class responsible for generating documentation files from GraphQL schema entities.
* Handles the conversion of schema types to markdown/MDX documentation with proper organization.
*
* HIERARCHY LEVELS WHEN categorySort IS ENABLED:
* - Level 0 (root): Query, Mutation, Subscription, Custom Groups → 01-Query, 02-Mutation, etc.
* - Level 1 (under root): Specific types within each root → 01-Objects, 02-Enums, etc.
*
* Each level has its own CategoryPositionManager that restarts numbering at 1.
* @example
*/
class Renderer {
group;
outputDir;
baseURL;
prettify;
options;
mdxExtension;
// mdxModuleIndexFileSupport: boolean;
printer;
rootLevelPositionManager;
categoryPositionManager;
/**
* Creates a new Renderer instance.
*
* @param printer - The printer instance used to convert GraphQL types to markdown
* @param outputDir - Directory where documentation will be generated
* @param baseURL - Base URL for the documentation
* @param group - Optional grouping configuration for schema entities
* @param prettify - Whether to format the generated markdown
* @param docOptions - Additional documentation options
* @param mdxExtension - Optional MDX file extension to use
* @example
*/
constructor(printer, outputDir, baseURL, group, prettify, docOptions, mdxExtension) {
this.printer = printer;
this.group = group;
this.outputDir = outputDir;
this.baseURL = baseURL;
this.prettify = prettify;
this.options = docOptions;
this.mdxExtension = mdxExtension;
// Initialize position managers for different hierarchy levels
// rootLevelPositionManager: for root-level categories (Query, Mutation, Deprecated, etc.)
this.rootLevelPositionManager = new CategoryPositionManager(docOptions?.categorySort, 1);
// categoryPositionManager: for categories within each root type (e.g., under Query)
this.categoryPositionManager = new CategoryPositionManager(docOptions?.categorySort, 1);
}
/**
* Generates an index metafile for a category directory if MDX support is available.
*
* @param dirPath - The directory path where the index should be created
* @param category - The category name
* @param options - Configuration options for the index
* @returns Promise that resolves when the index is generated
*
* @example
* ```typescript
* await renderer.generateIndexMetafile('docs/types', 'Types', {
* collapsible: true,
* collapsed: false
* });
* ```
*/
async generateIndexMetafile(dirPath, category, options) {
const defaultOptions = {
collapsible: true,
collapsed: true,
};
const finalOptions = options ?? defaultOptions;
const sidebarPosition = finalOptions.sidebarPosition ??
this.categoryPositionManager.getPosition(category);
const events = (0, event_emitter_1.getEvents)();
const event = new events_1.GenerateIndexMetafileEvent({
dirPath,
category,
options: {
...finalOptions,
sidebarPosition,
index: this.options?.index,
},
});
const handlerErrors = await events.emitAsync(events_1.GenerateIndexMetafileEvents.BEFORE_GENERATE, event);
if (Array.isArray(handlerErrors) && handlerErrors.length > 0) {
handlerErrors.forEach((error) => {
(0, logger_1.log)(`Error handler for ${events_1.GenerateIndexMetafileEvents.BEFORE_GENERATE}: ${error.message}`, logger_1.LogLevel.error);
});
}
await events.emitAsync(events_1.GenerateIndexMetafileEvents.AFTER_GENERATE, new events_1.GenerateIndexMetafileEvent({ dirPath, category }));
}
/**
* Generates the directory path and metafiles for a specific schema entity type.
* Creates the appropriate directory structure based on configuration options.
*
* @param type - The schema entity type
* @param name - The name of the schema entity
* @param rootTypeName - The root type name this entity belongs to
* @returns The generated directory path
* @example
*/
async generateCategoryMetafileType(type, name, rootTypeName) {
let dirPath = this.outputDir;
if (!(0, validation_1.isPath)(dirPath)) {
throw new Error("Output directory is empty or not specified");
}
if (isHierarchy(this.options, config_1.TypeHierarchy.FLAT)) {
return dirPath;
}
// Deprecated gets highest priority - at ROOT level before even custom groups
if (this.options?.deprecated === "group" && (0, graphql_1.isDeprecated)(type)) {
const formattedDeprecated = this.formatCategoryFolderName(DEPRECATED, true);
dirPath = (0, node_path_1.join)(dirPath, formattedDeprecated);
await this.generateIndexMetafile(dirPath, DEPRECATED, {
sidebarPosition: SidebarPosition.LAST,
styleClass: CATEGORY_STYLE_CLASS.DEPRECATED,
});
}
// Custom groups come after deprecated but before hierarchy levels
if (this.group &&
rootTypeName in this.group &&
name in this.group[rootTypeName]) {
const rootGroup = this.group[rootTypeName][name] ?? "";
// Custom groups are always at ROOT level (they come after deprecated in hierarchy priority)
const formattedRootGroup = this.formatCategoryFolderName(rootGroup, true);
dirPath = (0, node_path_1.join)(dirPath, formattedRootGroup);
await this.generateIndexMetafile(dirPath, rootGroup);
}
const useApiGroup = isHierarchy(this.options, config_1.TypeHierarchy.API)
? this.options.hierarchy[config_1.TypeHierarchy.API]
: !isHierarchy(this.options, config_1.TypeHierarchy.ENTITY);
if (useApiGroup) {
const typeCat = (0, exports.getApiGroupFolder)(type, useApiGroup);
// API groups are root-level if no custom groups exist, nested if custom groups exist
const isApiGroupRootLevel = !this.group;
const formattedTypeCat = this.formatCategoryFolderName(typeCat, isApiGroupRootLevel);
dirPath = (0, node_path_1.join)(dirPath, formattedTypeCat);
await this.generateIndexMetafile(dirPath, typeCat, {
collapsible: false,
collapsed: false,
styleClass: CATEGORY_STYLE_CLASS.API,
});
}
// Entity categories are:
// - Root-level in entity hierarchy (only when no custom groups exist)
// - Nested in API hierarchy
// - Nested in entity hierarchy when custom groups exist
const isRootTypeLevelCat = useApiGroup ? false : !this.group;
const formattedRootTypeName = this.formatCategoryFolderName(rootTypeName, isRootTypeLevelCat);
dirPath = (0, node_path_1.join)(dirPath, formattedRootTypeName);
await this.generateIndexMetafile(dirPath, rootTypeName);
return dirPath;
}
/**
* Renders all types within a root type category (e.g., all Query types).
*
* @param rootTypeName - The name of the root type (e.g., "Query", "Mutation")
* @param type - The type object containing all entities to render
* @returns Array of rendered categories or undefined
* @example
*/
async renderRootTypes(rootTypeName, type) {
if (typeof type !== "object" || type === null) {
return undefined;
}
const isFlat = isHierarchy(this.options, config_1.TypeHierarchy.FLAT);
const isOperationRootType = rootTypeName === "queries" ||
rootTypeName === "mutations" ||
rootTypeName === "subscriptions";
return Promise.all(Object.keys(type).map(async (name) => {
let dirPath = this.outputDir;
let entityName = name;
let operationNamespaceParts;
if (isOperationRootType && name.includes(".")) {
const namespaceParts = name.split(".").filter(Boolean);
if (namespaceParts.length > 1) {
operationNamespaceParts = namespaceParts.slice(0, -1);
if (!isFlat) {
entityName = namespaceParts.at(-1) ?? entityName;
}
}
}
if (!isFlat) {
dirPath = await this.generateCategoryMetafileType(type[name], name, rootTypeName);
if (operationNamespaceParts && operationNamespaceParts.length > 0) {
for (const namespace of operationNamespaceParts) {
const formattedNamespace = (0, utils_1.slugify)(namespace);
dirPath = (0, node_path_1.join)(dirPath, formattedNamespace);
await this.generateIndexMetafile(dirPath, namespace);
}
}
}
return this.renderTypeEntities(dirPath, entityName, type[name], operationNamespaceParts);
}));
}
/**
* Renders documentation for a specific type entity and saves it to a file.
*
* @param dirPath - The directory path where the file should be saved
* @param name - The name of the type entity
* @param type - The type entity to render
* @returns The category information for the rendered entity or undefined
* @example
*/
async renderTypeEntities(dirPath, name, type, operationNamespaceParts) {
if (!(0, validation_1.isPath)(dirPath)) {
throw new Error("Output directory is empty or not specified");
}
// Regex patterns to parse page metadata from file paths
// Matches: path/to/category/page-name.md(x) or path\to\category\page-name.md(x)
// Only allows alphanumeric and hyphens in category and page names
// NOSONAR - pattern is safe, hyphen in character class doesn't cause backtracking
const PageRegex = /(?<category>[a-z0-9-]+)[\\/]+(?<pageId>[a-z0-9-]+)\.mdx?$/i; // NOSONAR
const PageRegexFlat = /(?<pageId>[a-z0-9-]+)\.mdx?$/i; // NOSONAR
const fileName = (0, utils_1.slugify)(name);
const filePath = (0, node_path_1.join)((0, node_path_1.normalize)(dirPath), `${fileName}${this.mdxExtension}`);
let content;
try {
const printOptions = {
...this.options,
formatCategoryFolderName: (categoryName) => {
// Resolve root-level category names first. Group names can be stored in
// their original case in pre-collected categories, while link generation
// may provide slugified lowercase variants.
const rootCategory = this.rootLevelPositionManager.isRegistered(categoryName)
? categoryName
: (0, utils_1.startCase)(categoryName);
if (this.rootLevelPositionManager.isRegistered(rootCategory)) {
return this.formatCategoryFolderName(rootCategory, true);
}
// Nested categories can also be registered in either raw or start-cased
// forms depending on hierarchy inputs.
if (this.categoryPositionManager.isRegistered(categoryName)) {
return this.formatCategoryFolderName(categoryName, false);
}
const nestedCategory = (0, utils_1.startCase)(categoryName);
if (this.categoryPositionManager.isRegistered(nestedCategory)) {
return this.formatCategoryFolderName(nestedCategory, false);
}
// Unknown categories should not receive synthetic numeric prefixes.
return (0, utils_1.slugify)(categoryName);
},
...(operationNamespaceParts === undefined
? {}
: { operationNamespaceParts }),
};
content = await this.printer.printType(fileName, type, printOptions);
if (typeof content !== "string" || content === "") {
(0, logger_1.log)(`No content generated for "${name}" - printType returned empty`, logger_1.LogLevel.warn);
return undefined;
}
}
catch (error) {
(0, logger_1.log)(`An error occurred while processing "${name}": ${error instanceof Error ? error.message : String(error)}`, logger_1.LogLevel.warn);
return undefined;
}
await (0, utils_1.saveFile)(filePath, content, this.prettify ? utils_1.prettifyMarkdown : undefined);
const pagePath = (0, node_path_1.relative)(this.outputDir, filePath);
const isFlat = isHierarchy(this.options, config_1.TypeHierarchy.FLAT);
const page = isFlat
? PageRegexFlat.exec(pagePath)
: PageRegex.exec(pagePath);
if (!page?.groups) {
(0, logger_1.log)(`An error occurred while processing file ${filePath} for type "${type}"`, logger_1.LogLevel.warn);
return undefined;
}
// Strip numeric prefix from category if it was applied when categorySort is enabled
const extractedCategory = isFlat
? page.groups.pageId
: stripNumericPrefix(page.groups.category);
const category = isFlat ? "schema" : (0, utils_1.startCase)(extractedCategory);
const events = (0, event_emitter_1.getEvents)();
const event = new events_1.RenderTypeEntitiesEvent({
baseURL: this.baseURL,
name,
filePath,
outputDir: this.outputDir,
});
await events.emitAsync(events_1.RenderTypeEntitiesEvents.AFTER_RENDER, event);
return {
category,
filePath,
name,
};
}
/**
* Pre-collects all category names that will be generated during rendering.
* This allows the position manager to assign consistent positions before
* any files are written.
*
* HIERARCHY LEVELS:
* - Root level: Query, Mutation, Subscription, Deprecated (when grouped), custom root groups
* - Nested level: operations/types (API groups), custom groups under roots
*
* CRITICAL: Categories registered must match the NAMES USED BY THE PRINTER
* when generating links. The printer uses plural forms from ROOT_TYPE_LOCALE:
* "operations", "objects", "directives", "enums", "inputs", "interfaces",
* "mutations", "queries", "scalars", "subscriptions", "unions"
*
* NOT the folder names: "operations", "types"
*
* @param rootTypeNames - Array of root type names from the schema
*/
preCollectCategories(rootTypeNames) {
const rootCategories = new Set();
const nestedCategories = new Set();
// Skip if flat hierarchy
if (isHierarchy(this.options, config_1.TypeHierarchy.FLAT)) {
return;
}
// Determine if using API hierarchy
const useApiGroup = isHierarchy(this.options, config_1.TypeHierarchy.API)
? this.options.hierarchy[config_1.TypeHierarchy.API]
: !isHierarchy(this.options, config_1.TypeHierarchy.ENTITY);
// Register custom groups at root level
this.registerCustomGroups(rootCategories);
// Register categories based on hierarchy type
if (useApiGroup) {
this.registerApiGroupCategories(rootCategories, nestedCategories, this.group !== undefined);
}
else {
this.registerEntityCategories(rootCategories, nestedCategories, rootTypeNames, this.group !== undefined);
}
// Deprecated category - when grouped, it goes to root level
if (this.options?.deprecated === "group") {
rootCategories.add(DEPRECATED);
}
// Register collected categories with position managers
this.registerCategoriesWithManagers(rootCategories, nestedCategories);
}
/**
* Renders the homepage for the documentation from a template file.
* Replaces placeholders in the template with actual values.
*
* @param homepageLocation - Path to the homepage template file
* @returns Promise that resolves when the homepage is rendered
* @example
*/
async renderHomepage(homepageLocation) {
if (typeof homepageLocation !== "string") {
return;
}
if (!(0, validation_1.isPath)(this.outputDir)) {
throw new Error("Output directory is empty or not specified");
}
const homePage = (0, node_path_1.basename)(homepageLocation);
const destLocation = (0, node_path_1.join)(this.outputDir, homePage);
const slug = utils_1.pathUrl.resolve("/", this.baseURL);
try {
await (0, utils_1.copyFile)(homepageLocation, destLocation);
const template = await (0, utils_1.readFile)(destLocation);
const data = template
.toString()
.replaceAll("##baseURL##", slug)
.replaceAll("##generated-date-time##", new Date().toLocaleString());
await (0, utils_1.saveFile)(destLocation, data, this.prettify ? utils_1.prettifyMarkdown : undefined);
}
catch (error) {
(0, logger_1.log)(`An error occurred while processing the homepage ${homepageLocation}: ${error}`, logger_1.LogLevel.warn);
}
}
/**
* Registers custom group names from the current group configuration.
* Custom groups are always registered at the ROOT level.
*
* @param rootCategories - Set to add custom group names to
*/
registerCustomGroups(rootCategories) {
if (!this.group) {
return;
}
for (const rootTypeName in this.group) {
for (const name in this.group[rootTypeName]) {
const groupName = this.group[rootTypeName][name];
if (groupName) {
rootCategories.add(groupName);
}
}
}
}
/**
* Registers API group categories (operations/types) based on hierarchy and group configuration.
*
* @param rootCategories - Set to add root-level categories
* @param nestedCategories - Set to add nested-level categories
* @param hasCustomGroups - Whether custom groups are configured
*/
registerApiGroupCategories(rootCategories, nestedCategories, hasCustomGroups) {
if (hasCustomGroups) {
// Custom groups exist: operations/types are nested
nestedCategories.add(exports.API_GROUPS.operations);
nestedCategories.add(exports.API_GROUPS.types);
}
else {
// No custom groups: operations/types are at ROOT level
rootCategories.add(exports.API_GROUPS.operations);
rootCategories.add(exports.API_GROUPS.types);
}
// Entity categories for API group - plural forms from ROOT_TYPE_LOCALE
const entityCategoryNames = [
"directives",
"enums",
"inputs",
"interfaces",
"mutations",
"objects",
"queries",
"scalars",
"subscriptions",
"unions",
];
for (const categoryName of entityCategoryNames) {
nestedCategories.add(categoryName);
}
}
/**
* Registers entity hierarchy categories based on configuration.
*
* @param rootCategories - Set to add root-level categories
* @param nestedCategories - Set to add nested-level categories
* @param rootTypeNames - Array of root type names from schema
* @param hasCustomGroups - Whether custom groups are configured
*/
registerEntityCategories(rootCategories, nestedCategories, rootTypeNames, hasCustomGroups) {
for (const name of rootTypeNames) {
if (hasCustomGroups) {
// If custom groups exist, entity names go nested under them
nestedCategories.add(name);
}
else {
// If no custom groups, entity names are at root
rootCategories.add(name);
}
}
}
/**
* Registers collected categories with appropriate position managers.
* Handles both hierarchical numbering (when categorySort is enabled) and traditional modes.
*
* @param rootCategories - Set of root-level categories
* @param nestedCategories - Set of nested-level categories
*/
registerCategoriesWithManagers(rootCategories, nestedCategories) {
const hasCategorySort = this.options?.categorySort !== undefined;
if (hasCategorySort) {
// Register with hierarchical numbering when categorySort is enabled
this.rootLevelPositionManager.registerCategories(Array.from(rootCategories));
this.rootLevelPositionManager.computePositions();
this.categoryPositionManager.registerCategories(Array.from(nestedCategories));
this.categoryPositionManager.computePositions();
}
else {
// Traditional separate handling when categorySort is not defined
this.rootLevelPositionManager.registerCategories(Array.from(rootCategories));
this.rootLevelPositionManager.computePositions();
if (nestedCategories.size > 0) {
this.categoryPositionManager.registerCategories(Array.from(nestedCategories));
this.categoryPositionManager.computePositions();
}
}
}
/**
* Formats a category folder name, optionally prefixing with an order number.
*
* Supports hierarchical numbering:
* - Root level items (Query, Mutation, etc.) use rootLevelPositionManager
* - Nested items (groups, types within roots) use categoryPositionManager
*
* @param categoryName - The category name to format
* @param isRootLevel - Whether this category is at the root level (default: false for nested)
* @returns The formatted folder name (e.g., "01-objects" if prefix is enabled)
* @private
*/
formatCategoryFolderName(categoryName, isRootLevel = false) {
const hasCategorySort = this.options?.categorySort !== undefined;
if (!hasCategorySort) {
return (0, utils_1.slugify)(categoryName);
}
try {
// Choose the appropriate position manager based on hierarchy level
const manager = isRootLevel
? this.rootLevelPositionManager
: this.categoryPositionManager;
const position = manager.getPosition(categoryName);
if (!position) {
return (0, utils_1.slugify)(categoryName);
}
const paddedPosition = String(position).padStart(2, "0");
const slugifiedName = (0, utils_1.slugify)(categoryName);
const result = `${paddedPosition}-${slugifiedName}`;
return result;
}
catch {
return (0, utils_1.slugify)(categoryName);
}
}
}
exports.Renderer = Renderer;
/**
* Factory function to create and initialize a Renderer instance.
* Creates the output directory and returns a configured renderer.
*
* @param printer - The printer instance to use for rendering types
* @param outputDir - The output directory for generated documentation
* @param baseURL - The base URL for the documentation
* @param group - Optional grouping configuration
* @param prettify - Whether to prettify the output markdown
* @param docOptions - Additional documentation options
* @param mdxExtension - Extension to use for MDX files
* @returns A configured Renderer instance
*
* @example
* ```typescript
* const renderer = await getRenderer(
* myPrinter,
* './docs',
* '/api',
* groupConfig,
* true,
* { force: true, index: true }
* );
* ```
*/
const getRenderer = async (printer, outputDir, baseURL, group, prettify, docOptions, mdxExtension) => {
await (0, utils_1.ensureDir)(outputDir, { forceEmpty: docOptions?.force });
return new Renderer(printer, outputDir, baseURL, group, prettify, docOptions, mdxExtension);
};
exports.getRenderer = getRenderer;