vitepress-openapi
Version:
Generate VitePress API Documentation from OpenAPI Specification.
172 lines (141 loc) • 5.17 kB
text/typescript
import type { OpenAPIV3 } from '@scalar/openapi-types'
import type { PluginWithOptions } from 'markdown-it'
import { useOpenapi } from '../../composables/useOpenapi'
export interface OperationLinkPluginOptions {
/**
* Prefix for operation links
* @default '/operations/'
*/
linkPrefix?: string
/**
* Function to transform the href before rendering
* @param href The original href
* @returns The transformed href
*/
transformHref?: (href: string) => string
/**
* Custom function to create the HTML for operation links
* @param href The transformed href
* @param method The HTTP method of the operation
* @param title The title of the operation
* @returns The HTML string for the operation link
*/
createOperationLinkHtml?: (href: string, method: string, title: string) => string
}
function createDefaultRenderer(
tokens: any[],
idx: number,
options: any,
env: any,
self: any,
): string {
return self.renderToken(tokens, idx, options)
}
function extractOperationId(href: string, linkPrefix: string): string | null {
if (!href.startsWith(linkPrefix)) {
return null
}
const parts = href.substring(linkPrefix.length).split('/')
return parts.length >= 1 ? parts[0] : null
}
function getOperationTitle(
tokens: any[],
idx: number,
operation: OpenAPIV3.OperationObject | undefined,
): string {
// Get the link text from the next token if it's a text token.
let title = ''
if (idx + 1 < tokens.length && tokens[idx + 1].type === 'text') {
title = tokens[idx + 1].content
}
// Fallback to operation summary or operationId if no title is provided.
return title || operation?.summary || operation?.operationId || ''
}
function createOperationLinkHtml(
href: string,
method: string,
title: string,
): string {
return `<a href="${href}" class="OAOperationLink group/oaOperationLink">`
+ `<span class="OAOperationLink-badge OAMethodBadge--${method.toLowerCase()}">${method.toUpperCase()}</span>`
+ `<span class="OAOperationLink-title">${title}</span>`
+ `</a>`
}
function modifyTokensForOperationLink(
tokens: any[],
idx: number,
html: string,
): void {
// Replace the token content with the generated HTML.
tokens[idx].type = 'html_inline'
tokens[idx].content = html
tokens[idx].children = []
// Skip the next token if it's the text token we used for the title.
if (idx + 1 < tokens.length && tokens[idx + 1].type === 'text') {
tokens[idx + 1].content = ''
}
// Skip the link_close token.
// For links with text, link_close is at idx+2.
if (idx + 2 < tokens.length && tokens[idx + 2].type === 'link_close') {
tokens[idx + 2].type = 'text'
tokens[idx + 2].content = ''
}
// For links without text, link_close is at idx+1.
else if (idx + 1 < tokens.length && tokens[idx + 1].type === 'link_close') {
tokens[idx + 1].type = 'text'
tokens[idx + 1].content = ''
}
}
/**
* Markdown-it plugin that transforms links with a specific prefix into operation links
* with method badges and proper styling.
*/
const operationLink: PluginWithOptions<OperationLinkPluginOptions> = (md, options = {}) => {
const defaultRender = md.renderer.rules.link_open || createDefaultRenderer
const defaultLinkCloseRender = md.renderer.rules.link_close || createDefaultRenderer
const {
linkPrefix = '/operations/',
transformHref,
createOperationLinkHtml: customCreateOperationLinkHtml,
} = options
const openapi = useOpenapi()
// Override the link renderer.
md.renderer.rules.link_open = function (tokens, idx, options, env, self) {
const hrefIndex = tokens[idx].attrIndex('href')
if (hrefIndex < 0) {
return defaultRender(tokens, idx, options, env, self)
}
const href = tokens[idx].attrs![hrefIndex][1]
const operationId = extractOperationId(href, linkPrefix)
if (!operationId) {
return defaultRender(tokens, idx, options, env, self)
}
const operation = openapi.getOperation?.(operationId)
if (!operation) {
return defaultRender(tokens, idx, options, env, self)
}
const method = openapi.getOperationMethod?.(operationId) || 'get'
const title = getOperationTitle(tokens, idx, operation)
let transformedHref = href
if (transformHref) {
transformedHref = transformHref(href)
tokens[idx].attrs![hrefIndex][1] = transformedHref
}
const html = customCreateOperationLinkHtml
? customCreateOperationLinkHtml(transformedHref, method, title)
: createOperationLinkHtml(transformedHref, method, title)
modifyTokensForOperationLink(tokens, idx, html)
return html
}
// We don't need the link_close handler anymore since we're replacing the entire link.
md.renderer.rules.link_close = function (tokens, idx, options, env, self) {
// If the token has been modified to be an empty text, return an empty string.
if (tokens[idx].type === 'text' && tokens[idx].content === '') {
return ''
}
// Otherwise use the default renderer.
return defaultLinkCloseRender(tokens, idx, options, env, self)
}
}
export { operationLink }
export default operationLink