in src/server/remarkPlugins/swagger/index.ts [115:216]
async function processLinkNode(target: Target, context: Context) {
const [node] = target;
if (!node.url || !node.url.startsWith("swagger:")) {
return;
}
const pathname = node.url.split('?')[0].slice("swagger:".length);
const queryString = node.url.split('?')[1];
const queryParams = queryString
? new Map(queryString.split('&').map(param => {
const [key, value = ''] = param.split('=');
return [decodeURIComponent(key).replace(/\+/g, ' '), decodeURIComponent(value).replace(/\+/g, ' ')] as const;
}))
: new Map();
const relativePath = path.relative(context.baseDir, context.filePath);
// Split the path into segments
const segments = pathname.split('/').filter(Boolean);
// Parse the path segments
const isAdmin = segments[0] === 'admin';
if (!isAdmin || segments.length < 2) {
throw new Error(`Invalid swagger URL format: ${pathname}`);
}
// Extract components
let apiVersion = segments[1];
let apiType: SwaggerApiType = 'default';
let operationName: string;
if (segments.length === 3) {
operationName = segments[2];
} else {
apiType = segments[2] as SwaggerApiType;
operationName = segments[3];
}
const position = node.position ? ` ${relativePath} at position ${node.position.start.line}:${node.position.start.column}` : '';
// Validate the subContext is a valid SwaggerFileType
if (apiType !== 'default' && !getSwaggerFileName(apiType)) {
throw new Error(`Invalid swagger sub-context: ${apiType}${position}`);
}
const swaggerResult = context.cache.getSwaggerResult(relativePath, apiType);
const swaggerJson = swaggerResult.json;
// Search through all paths in the swagger JSON
let matches: { path: string; method: string; tags: string[]; summary?: string }[] = [];
const tagParam = queryParams.get('tag') ||
(operationName.startsWith('PersistentTopics_') ? '^persistent' : undefined);
const summaryParam = queryParams.get('summary');
// Handle negation separately from regex pattern
const isTagNegated = tagParam?.startsWith('!') || false;
const isSummaryNegated = summaryParam?.startsWith('!') || false;
// Convert params to regex patterns without negation
const tagRegex = tagParam && new RegExp(isTagNegated ? tagParam.slice(1) : tagParam);
const summaryRegex = summaryParam && new RegExp(isSummaryNegated ? summaryParam.slice(1) : summaryParam);
for (const [path, methods] of Object.entries(swaggerJson.paths || {})) {
for (const [method, operation] of Object.entries(methods as Record<string, {
operationId: string;
tags: string[];
summary?: string;
}>)) {
const tagMatches = !tagRegex || (operation.tags &&operation.tags.some(t => tagRegex.test(t)) !== isTagNegated);
const summaryMatches = !summaryRegex || (operation.summary && summaryRegex.test(operation.summary) !== isSummaryNegated);
if (operation.operationId === operationName && tagMatches && summaryMatches) {
matches.push({ path, method, tags: operation.tags, summary: operation.summary });
}
}
}
if (matches.length === 0) {
throw new Error(`Operation ${operationName} tag:${tagParam} summary:${summaryParam} not found in swagger JSON${position}`);
}
if (matches.length > 1) {
console.warn(`Multiple operations found for ${operationName} tag:${tagParam} summary:${summaryParam}:\n${matches.map(m => `${m.method} ${m.path} tags:${m.tags.join(',')} summary:${m.summary}`).join('\n')}${position}`);
}
// Select the match with the longest path since there are duplicates
const longestMatch = matches.reduce((longest, current) =>
current.path.length > longest.path.length ? current : longest
);
const foundPath = swaggerJson.basePath + longestMatch.path;
const foundMethod = longestMatch.method.toUpperCase();
const restApiBaseUrl = context.restApiBaseUrlMapping[apiType];
const swaggerVersion = swaggerResult.version;
node.url = `${restApiBaseUrl}?version=${swaggerVersion}&apiVersion=${apiVersion}#operation/${operationName}`;
node.children = [{
type: "text",
value: `${foundMethod} ${foundPath}`
}];
}