import type { Context, Page } from '@/types' import type { PageTransformer } from './types' import type { Operation } from '@/rest/components/types' import { renderContent } from '@/content-render/index' import matter from '@gr2m/gray-matter' import { readFileSync } from 'fs' import { join, dirname } from 'path' import { fileURLToPath } from 'url' import { fastTextOnly } from '@/content-render/unified/text-only' const __filename = fileURLToPath(import.meta.url) const __dirname = dirname(__filename) /** * Transformer for REST API pages * Converts REST operations and their data into markdown format using a Liquid template */ export class RestTransformer implements PageTransformer { canTransform(page: Page): boolean { // Only transform REST pages that are not landing pages // Landing pages (like /en/rest) will be handled by a separate transformer return page.autogenerated === 'rest' && !page.relativePath.endsWith('index.md') } async transform( page: Page, pathname: string, context: Context, apiVersion?: string, ): Promise { // Import getRest dynamically to avoid circular dependencies const { default: getRest } = await import('@/rest/lib/index') // Extract version from context const currentVersion = context.currentVersion! // Use the provided apiVersion, or fall back to the latest from context const effectiveApiVersion = apiVersion || (context.currentVersionObj?.apiVersions?.length ? context.currentVersionObj.latestApiVersion : undefined) // Parse the category and subcategory from the page path // e.g. /en/rest/actions/artifacts -> category: actions, subcategory: artifacts const pathParts = pathname.split('/').filter(Boolean) const restIndex = pathParts.indexOf('rest') if (restIndex === -1 || restIndex >= pathParts.length - 1) { throw new Error(`Invalid REST path: ${pathname}`) } const category = pathParts[restIndex + 1] const subcategory = pathParts[restIndex + 2] // May be undefined for category-only pages // Get the REST operations data const restData = await getRest(currentVersion, effectiveApiVersion) let operations: Operation[] = [] if (subcategory && restData[category]?.[subcategory]) { operations = restData[category][subcategory] } else if (category && restData[category]) { // For categories without subcategories, operations are nested directly const categoryData = restData[category] // Flatten all operations from all subcategories operations = Object.values(categoryData).flat() } // Prepare manual content let manualContent = '' if (page.markdown) { const markerIndex = page.markdown.indexOf( '', ) if (markerIndex > 0) { const { content } = matter(page.markdown) const manualContentMarkerIndex = content.indexOf( '', ) if (manualContentMarkerIndex > 0) { const rawManualContent = content.substring(0, manualContentMarkerIndex).trim() if (rawManualContent) { manualContent = await renderContent(rawManualContent, { ...context, markdownRequested: true, }) } } } } // Prepare data for template const templateData = await this.prepareTemplateData( page, operations, context, manualContent, effectiveApiVersion, ) // Load and render template const templatePath = join(__dirname, '../templates/rest-page.template.md') const templateContent = readFileSync(templatePath, 'utf8') // Render the template with Liquid const rendered = await renderContent(templateContent, { ...context, ...templateData, markdownRequested: true, }) return rendered } /** * Prepare data for the Liquid template */ private async prepareTemplateData( page: Page, operations: Operation[], context: Context, manualContent: string, apiVersion?: string, ): Promise> { // Prepare page intro const intro = page.intro ? await page.renderProp('intro', context, { textOnly: true }) : '' // Prepare operations for the template const preparedOperations = await Promise.all( operations.map(async (operation) => await this.prepareOperation(operation)), ) return { page: { title: page.title, intro, }, manualContent, restOperations: preparedOperations, apiVersion, } } /** * Prepare a single operation for template rendering */ private async prepareOperation(operation: Operation): Promise> { // Convert HTML description to text const description = operation.descriptionHTML ? fastTextOnly(operation.descriptionHTML) : '' // Determine header settings const needsContentTypeHeader = operation.subcategory === 'inference' const omitHeaders = operation.subcategory === 'management-console' || operation.subcategory === 'manage-ghes' const showHeaders = !omitHeaders // Check if operation has parameters const hasParameters = (operation.parameters?.length || 0) > 0 || (operation.bodyParameters?.length || 0) > 0 // Process status codes to convert HTML descriptions to plain text const statusCodes = operation.statusCodes?.map((statusCode) => ({ ...statusCode, description: statusCode.description ? fastTextOnly(statusCode.description) : undefined, })) // Prepare code examples with processed URLs const codeExamples = operation.codeExamples?.map((example) => { let url = `${operation.serverUrl}${operation.requestPath}` // Replace path parameters in URL if (example.request?.parameters && Object.keys(example.request.parameters).length > 0) { for (const [key, value] of Object.entries(example.request.parameters)) { url = url.replace(`{${key}}`, String(value)) } } return { request: { description: example.request?.description ? fastTextOnly(example.request.description) : '', url, acceptHeader: example.request?.acceptHeader, bodyParameters: example.request?.bodyParameters ? JSON.stringify(example.request.bodyParameters, null, 2) : null, }, response: { statusCode: example.response?.statusCode, schema: (example.response as any)?.schema ? JSON.stringify((example.response as any).schema, null, 2) : null, }, } }) || [] return { ...operation, description, hasParameters, showHeaders, needsContentTypeHeader, statusCodes, codeExamples, } } }