chore: import upstream snapshot with attribution
docmd CI verification / verify (push) Failing after 0s
docmd CI verification / verify (push) Failing after 0s
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025-present docmd.io
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,17 @@
|
||||
# @docmd/plugin-openapi
|
||||
|
||||
Render OpenAPI 3.x API reference documentation directly from JSON or YAML spec files inside your Markdown pages at build time. Core plugin, already included in the core.
|
||||
|
||||
```bash
|
||||
docmd add openapi
|
||||
```
|
||||
|
||||
Part of the **[docmd](https://github.com/docmd-io/docmd)** documentation engine.
|
||||
|
||||
## Documentation
|
||||
|
||||
See **[docs.docmd.io](https://docs.docmd.io)** for full usage and API reference.
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* --------------------------------------------------------------------
|
||||
* docmd : the zero-config documentation engine.
|
||||
*
|
||||
* @package @docmd/core (and ecosystem)
|
||||
* @website https://docmd.io
|
||||
* @repository https://github.com/docmd-io/docmd
|
||||
* @license MIT
|
||||
* @copyright Copyright (c) 2025-present docmd.io
|
||||
*
|
||||
* [docmd-source] - Please do not remove this header.
|
||||
* --------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
// Ensure dist exists
|
||||
if (!fs.existsSync(path.join(__dirname, 'dist'))) {
|
||||
fs.mkdirSync(path.join(__dirname, 'dist'));
|
||||
}
|
||||
|
||||
// Copy CSS
|
||||
fs.copyFileSync(
|
||||
path.join(__dirname, 'src', 'openapi.css'),
|
||||
path.join(__dirname, 'dist', 'openapi.css')
|
||||
);
|
||||
|
||||
console.log('OpenAPI assets copied to dist/');
|
||||
@@ -0,0 +1,70 @@
|
||||
{
|
||||
"name": "@docmd/plugin-openapi",
|
||||
"version": "0.8.12",
|
||||
"description": "OpenAPI documentation generator plugin for docmd.",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"docmd": {
|
||||
"key": "openapi",
|
||||
"kind": "plugin",
|
||||
"displayName": "OpenAPI",
|
||||
"tagline": "OpenAPI / Swagger spec rendering for docmd.",
|
||||
"capabilities": [
|
||||
"markdown",
|
||||
"assets"
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc -p tsconfig.json && node build.js"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@docmd/api": "workspace:*"
|
||||
},
|
||||
"dependencies": {
|
||||
"@docmd/utils": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@docmd/api": "workspace:*",
|
||||
"@types/node": "^24.13.3"
|
||||
},
|
||||
"keywords": [
|
||||
"docmd",
|
||||
"plugin",
|
||||
"openapi",
|
||||
"swagger",
|
||||
"rest",
|
||||
"api-docs",
|
||||
"documentation",
|
||||
"markdown",
|
||||
"static-site-generator",
|
||||
"zero-config",
|
||||
"docs"
|
||||
],
|
||||
"author": {
|
||||
"name": "Ghazi",
|
||||
"url": "https://mgks.dev"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/docmd-io/docmd.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/docmd-io/docmd/issues"
|
||||
},
|
||||
"homepage": "https://docmd.io",
|
||||
"funding": "https://github.com/sponsors/mgks",
|
||||
"license": "MIT"
|
||||
}
|
||||
@@ -0,0 +1,389 @@
|
||||
/**
|
||||
* --------------------------------------------------------------------
|
||||
* docmd : the zero-config documentation engine.
|
||||
*
|
||||
* @package @docmd/core (and ecosystem)
|
||||
* @website https://docmd.io
|
||||
* @repository https://github.com/docmd-io/docmd
|
||||
* @license MIT
|
||||
* @copyright Copyright (c) 2025-present docmd.io
|
||||
*
|
||||
* [docmd-source] - Please do not remove this header.
|
||||
* --------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { createRequire } from 'module';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { safePath, asUserPath } from '@docmd/utils';
|
||||
import type { PluginDescriptor } from '@docmd/api';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
export const plugin: PluginDescriptor = {
|
||||
name: 'openapi',
|
||||
version: '0.8.12',
|
||||
capabilities: ['markdown', 'assets']
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types (minimal OpenAPI 3.x subset)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface OASchema {
|
||||
type?: string;
|
||||
format?: string;
|
||||
description?: string;
|
||||
properties?: Record<string, OASchema>;
|
||||
items?: OASchema;
|
||||
enum?: (string | number)[];
|
||||
required?: string[];
|
||||
$ref?: string;
|
||||
example?: unknown;
|
||||
default?: unknown;
|
||||
nullable?: boolean;
|
||||
oneOf?: OASchema[];
|
||||
anyOf?: OASchema[];
|
||||
allOf?: OASchema[];
|
||||
}
|
||||
|
||||
interface OAParameter {
|
||||
name: string;
|
||||
in: 'query' | 'path' | 'header' | 'cookie';
|
||||
description?: string;
|
||||
required?: boolean;
|
||||
schema?: OASchema;
|
||||
example?: unknown;
|
||||
}
|
||||
|
||||
interface OAMediaType {
|
||||
schema?: OASchema;
|
||||
example?: unknown;
|
||||
}
|
||||
|
||||
interface OARequestBody {
|
||||
description?: string;
|
||||
required?: boolean;
|
||||
content?: Record<string, OAMediaType>;
|
||||
}
|
||||
|
||||
interface OAResponse {
|
||||
description?: string;
|
||||
content?: Record<string, OAMediaType>;
|
||||
}
|
||||
|
||||
interface OAOperation {
|
||||
operationId?: string;
|
||||
summary?: string;
|
||||
description?: string;
|
||||
tags?: string[];
|
||||
parameters?: OAParameter[];
|
||||
requestBody?: OARequestBody;
|
||||
responses?: Record<string, OAResponse>;
|
||||
deprecated?: boolean;
|
||||
}
|
||||
|
||||
interface OAPathItem {
|
||||
get?: OAOperation;
|
||||
post?: OAOperation;
|
||||
put?: OAOperation;
|
||||
patch?: OAOperation;
|
||||
delete?: OAOperation;
|
||||
head?: OAOperation;
|
||||
options?: OAOperation;
|
||||
}
|
||||
|
||||
interface OASpec {
|
||||
openapi?: string;
|
||||
info?: { title?: string; version?: string; description?: string };
|
||||
paths?: Record<string, OAPathItem>;
|
||||
components?: { schemas?: Record<string, OASchema> };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const HTTP_METHODS = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options'] as const;
|
||||
|
||||
const METHOD_COLORS: Record<string, string> = {
|
||||
get: '#10b981',
|
||||
post: '#3b82f6',
|
||||
put: '#f59e0b',
|
||||
patch: '#8b5cf6',
|
||||
delete: '#ef4444',
|
||||
head: '#6b7280',
|
||||
options: '#6b7280'
|
||||
};
|
||||
|
||||
function esc(str: string): string {
|
||||
return String(str)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
}
|
||||
|
||||
function describe(str: string | undefined, options: any): string {
|
||||
if (!str) return '';
|
||||
return options?.allowRawHtml ? str : esc(str);
|
||||
}
|
||||
|
||||
/** Resolve a $ref like #/components/schemas/Foo against the spec */
|
||||
function resolveRef(ref: string, spec: OASpec): OASchema | null {
|
||||
if (!ref.startsWith('#/')) return null;
|
||||
const parts = ref.slice(2).split('/');
|
||||
let node: any = spec;
|
||||
for (const part of parts) {
|
||||
if (node == null || typeof node !== 'object') return null;
|
||||
node = node[part];
|
||||
}
|
||||
return node as OASchema | null;
|
||||
}
|
||||
|
||||
function resolveSchema(schema: OASchema | undefined, spec: OASpec, depth = 0): OASchema {
|
||||
if (!schema) return {};
|
||||
if (schema.$ref) return resolveRef(schema.$ref, spec) || schema;
|
||||
return schema;
|
||||
}
|
||||
|
||||
/** Render a schema as a compact type string */
|
||||
function typeLabel(schema: OASchema | undefined, spec: OASpec): string {
|
||||
if (!schema) return 'any';
|
||||
const resolved = resolveSchema(schema, spec);
|
||||
if (resolved.$ref) return resolved.$ref.split('/').pop() || 'object';
|
||||
if (resolved.type === 'array') return `array[${typeLabel(resolved.items, spec)}]`;
|
||||
if (resolved.oneOf) return resolved.oneOf.map(s => typeLabel(s, spec)).join(' | ');
|
||||
if (resolved.anyOf) return resolved.anyOf.map(s => typeLabel(s, spec)).join(' | ');
|
||||
if (resolved.enum) return resolved.enum.map(v => `"${v}"`).join(' | ');
|
||||
return [resolved.type, resolved.format].filter(Boolean).join(':') || 'any';
|
||||
}
|
||||
|
||||
/** Render schema properties as an HTML table */
|
||||
function renderSchemaTable(schema: OASchema | undefined, spec: OASpec, options: any): string {
|
||||
if (!schema) return '';
|
||||
const resolved = resolveSchema(schema, spec);
|
||||
const props = resolved.properties;
|
||||
if (!props || Object.keys(props).length === 0) return '';
|
||||
|
||||
const required = new Set(resolved.required || []);
|
||||
const rows = Object.entries(props).map(([name, prop]) => {
|
||||
const r = resolveSchema(prop, spec);
|
||||
return `<tr>
|
||||
<td><code>${esc(name)}</code>${required.has(name) ? ' <span class="oa-required">*</span>' : ''}</td>
|
||||
<td><span class="oa-type">${esc(typeLabel(prop, spec))}</span></td>
|
||||
<td>${describe(r.description, options)}</td>
|
||||
<td>${r.default !== undefined ? `<code>${esc(String(r.default))}</code>` : ''}</td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
|
||||
return `<table class="oa-schema-table">
|
||||
<thead><tr><th>Field</th><th>Type</th><th>Description</th><th>Default</th></tr></thead>
|
||||
<tbody>${rows}</tbody>
|
||||
</table>`;
|
||||
}
|
||||
|
||||
/** Render a single operation */
|
||||
function renderOperation(method: string, path_: string, op: OAOperation, spec: OASpec, options: any): string {
|
||||
const color = METHOD_COLORS[method] || '#6b7280';
|
||||
const deprecated = op.deprecated ? '<span class="oa-deprecated">DEPRECATED</span>' : '';
|
||||
const summaryOnly = options?.summaryOnly === true;
|
||||
|
||||
// Parameters
|
||||
let paramsHtml = '';
|
||||
if (!summaryOnly && op.parameters && op.parameters.length > 0) {
|
||||
const rows = op.parameters.map(p => {
|
||||
const r = resolveSchema(p.schema, spec);
|
||||
return `<tr>
|
||||
<td><code>${esc(p.name)}</code>${p.required ? ' <span class="oa-required">*</span>' : ''}</td>
|
||||
<td><span class="oa-param-in">${esc(p.in)}</span></td>
|
||||
<td><span class="oa-type">${esc(typeLabel(p.schema, spec))}</span></td>
|
||||
<td>${describe(p.description, options)}</td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
paramsHtml = `<h5>Parameters</h5>
|
||||
<table class="oa-schema-table">
|
||||
<thead><tr><th>Name</th><th>In</th><th>Type</th><th>Description</th></tr></thead>
|
||||
<tbody>${rows}</tbody>
|
||||
</table>`;
|
||||
}
|
||||
|
||||
// Request body
|
||||
let requestHtml = '';
|
||||
if (op.requestBody?.content) {
|
||||
const entries = Object.entries(op.requestBody.content);
|
||||
requestHtml = `<h5>Request Body${op.requestBody.required ? ' <span class="oa-required">*</span>' : ''}</h5>`;
|
||||
for (const [contentType, media] of entries) {
|
||||
requestHtml += `<p class="oa-content-type"><code>${esc(contentType)}</code></p>`;
|
||||
requestHtml += renderSchemaTable(media.schema, spec, options);
|
||||
}
|
||||
}
|
||||
|
||||
// Responses
|
||||
let responsesHtml = '';
|
||||
if (!summaryOnly && op.responses) {
|
||||
const statusCodes = Object.entries(op.responses);
|
||||
const rows = statusCodes.map(([code, resp]) => {
|
||||
const cls = code.startsWith('2') ? 'oa-status-ok' : code.startsWith('4') ? 'oa-status-err' : 'oa-status-other';
|
||||
let schemaInfo = '';
|
||||
if (resp.content) {
|
||||
const firstMedia = Object.values(resp.content)[0];
|
||||
if (firstMedia?.schema) schemaInfo = `<br><span class="oa-type">${esc(typeLabel(firstMedia.schema, spec))}</span>`;
|
||||
}
|
||||
return `<tr>
|
||||
<td><span class="oa-status-badge ${cls}">${esc(code)}</span></td>
|
||||
<td>${describe(resp.description, options)}${schemaInfo}</td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
responsesHtml = `<h5>Responses</h5>
|
||||
<table class="oa-schema-table">
|
||||
<thead><tr><th>Status</th><th>Description</th></tr></thead>
|
||||
<tbody>${rows}</tbody>
|
||||
</table>`;
|
||||
}
|
||||
|
||||
const id = `oa-${method}-${path_.replace(/[^a-z0-9]/gi, '-').toLowerCase()}`;
|
||||
|
||||
return `<div class="oa-operation" id="${esc(id)}">
|
||||
<div class="oa-operation-header">
|
||||
<span class="oa-method" style="background:${color}">${method.toUpperCase()}</span>
|
||||
<code class="oa-path">${esc(path_)}</code>
|
||||
${deprecated}
|
||||
</div>
|
||||
${op.summary ? `<p class="oa-summary">${esc(op.summary)}</p>` : ''}
|
||||
${op.description ? `<p class="oa-description">${describe(op.description, options)}</p>` : ''}
|
||||
${paramsHtml}
|
||||
${requestHtml}
|
||||
${responsesHtml}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
/** Parse OpenAPI spec from a file path (JSON or YAML). specPath is validated
|
||||
* upstream by safePath() in renderSpec(); this function assumes it is safe. */
|
||||
function parseSpec(specPath: string): OASpec {
|
||||
// eslint-disable-next-line docmd/no-unsafe-fs-read -- specPath is validated by safePath() in renderSpec()
|
||||
const raw = fs.readFileSync(specPath, 'utf8');
|
||||
// JSON detection
|
||||
const trimmed = raw.trimStart();
|
||||
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
|
||||
return JSON.parse(raw);
|
||||
}
|
||||
// Minimal YAML parser for OpenAPI specs (handles the common subset)
|
||||
// We avoid a full YAML dep by using JSON if possible, otherwise note the limitation
|
||||
try {
|
||||
// Try to require js-yaml if available (won't throw at import time since it's optional)
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const yaml = require('js-yaml');
|
||||
return yaml.load(raw) as OASpec;
|
||||
} catch {
|
||||
throw new Error(`OpenAPI plugin: YAML spec at "${specPath}" requires js-yaml to be installed.\nRun: npm install js-yaml`);
|
||||
}
|
||||
}
|
||||
|
||||
/** Render full spec as HTML */
|
||||
function renderSpec(specPath: string, rootDir: string, options: any): string {
|
||||
// Phase 1.A: CWE-22 fix (S-2, N-S2). Use safePath() to enforce boundary.
|
||||
// Absolute paths and ../ traversal are rejected by safePath() (throws).
|
||||
let absPath: string;
|
||||
try {
|
||||
absPath = safePath(rootDir, asUserPath(specPath));
|
||||
} catch (_e: any) {
|
||||
return `<div class="oa-error">OpenAPI spec path escapes project root: <code>${esc(specPath)}</code></div>`;
|
||||
}
|
||||
|
||||
if (!fs.existsSync(absPath)) {
|
||||
return `<div class="oa-error">OpenAPI spec not found: <code>${esc(specPath)}</code></div>`;
|
||||
}
|
||||
|
||||
let spec: OASpec;
|
||||
try {
|
||||
spec = parseSpec(absPath);
|
||||
} catch (e: any) {
|
||||
return `<div class="oa-error">Failed to parse OpenAPI spec: ${esc(String(e.message))}</div>`;
|
||||
}
|
||||
|
||||
const info = spec.info || {};
|
||||
let html = `<div class="oa-spec">`;
|
||||
|
||||
if (options?.info !== false && info.title) {
|
||||
const downloadLink = options?.download ? `<a href="${esc(specPath)}" class="oa-download-link" title="Download OpenAPI Spec" target="_blank">JSON / YAML</a>` : '';
|
||||
html += `<div class="oa-spec-header">
|
||||
<h2 class="oa-spec-title">${esc(info.title)}</h2>
|
||||
<div class="oa-spec-meta">
|
||||
${info.version ? `<span class="oa-spec-version">v${esc(info.version)}</span>` : ''}
|
||||
${downloadLink}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
if (info.description) {
|
||||
html += `<p class="oa-spec-description">${describe(info.description, options)}</p>`;
|
||||
}
|
||||
|
||||
if (spec.paths) {
|
||||
for (const [pathStr, pathItem] of Object.entries(spec.paths)) {
|
||||
for (const method of HTTP_METHODS) {
|
||||
const op = pathItem[method];
|
||||
if (op) {
|
||||
html += renderOperation(method, pathStr, op, spec, options);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
return html;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Plugin hooks
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Extend markdown-it to handle ```openapi fences.
|
||||
* Usage in markdown:
|
||||
*
|
||||
* ```openapi
|
||||
* ./path/to/spec.json
|
||||
* ```
|
||||
*/
|
||||
export function markdownSetup(md: any, options: any): void {
|
||||
const srcDir: string = options?.config?.src
|
||||
? path.resolve(process.cwd(), options.config.src)
|
||||
: process.cwd();
|
||||
|
||||
const originalFence = md.renderer.rules.fence || ((tokens: any[], idx: number, opts: any, _env: any, self: any) => self.renderToken(tokens, idx, opts));
|
||||
|
||||
md.renderer.rules.fence = (tokens: any[], idx: number, opts: any, env: any, self: any) => {
|
||||
const token = tokens[idx];
|
||||
const info = (token.info || '').trim();
|
||||
|
||||
if (info !== 'openapi') {
|
||||
return originalFence(tokens, idx, opts, env, self);
|
||||
}
|
||||
|
||||
const specPath = token.content.trim();
|
||||
const pluginOptions = options?.config?.plugins?.openapi || {};
|
||||
return renderSpec(specPath, srcDir, pluginOptions);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide OpenAPI CSS asset.
|
||||
*/
|
||||
export function getAssets(_options?: any): any[] {
|
||||
const distCssPath = path.resolve(__dirname, '..', 'dist', 'openapi.css');
|
||||
const srcCssPath = path.resolve(__dirname, '..', 'src', 'openapi.css');
|
||||
const cssPath = fs.existsSync(distCssPath) ? distCssPath : srcCssPath;
|
||||
|
||||
// Only inject if our bundled CSS exists
|
||||
if (!fs.existsSync(cssPath)) return [];
|
||||
|
||||
return [{
|
||||
src: cssPath,
|
||||
dest: 'assets/css/docmd-openapi.css',
|
||||
type: 'css',
|
||||
location: 'head'
|
||||
}];
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
/**
|
||||
* --------------------------------------------------------------------
|
||||
* docmd : the zero-config documentation engine.
|
||||
*
|
||||
* @package @docmd/core (and ecosystem)
|
||||
* @website https://docmd.io
|
||||
* @repository https://github.com/docmd-io/docmd
|
||||
* @license MIT
|
||||
* @copyright Copyright (c) 2025-present docmd.io
|
||||
*
|
||||
* [docmd-source] - Please do not remove this header.
|
||||
* --------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
.oa-spec {
|
||||
margin: 2rem 0;
|
||||
font-family: var(--font-family-sans, system-ui, -apple-system, sans-serif);
|
||||
}
|
||||
|
||||
.oa-spec-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
border-bottom: 2px solid var(--border-color, #eee);
|
||||
padding-bottom: 0.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.oa-spec-title {
|
||||
margin: 0;
|
||||
font-size: 1.75rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-heading, #111);
|
||||
}
|
||||
|
||||
.oa-spec-meta {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.oa-spec-version {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-muted, #666);
|
||||
background: var(--code-bg, #f3f4f6);
|
||||
padding: 0.125rem 0.5rem;
|
||||
border-radius: 9999px;
|
||||
}
|
||||
|
||||
.oa-download-link {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: var(--link-color, #3b82f6);
|
||||
text-decoration: none;
|
||||
border: 1px solid var(--link-color, #3b82f6);
|
||||
padding: 0.125rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.oa-download-link:hover {
|
||||
background: var(--link-color, #3b82f6);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.oa-spec-description {
|
||||
color: var(--text-muted, #666);
|
||||
line-height: 1.6;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.oa-operation {
|
||||
margin-bottom: 2rem;
|
||||
border: 1px solid var(--border-color, #eee);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background: var(--bg-color, #fff);
|
||||
}
|
||||
|
||||
.oa-operation-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 0.75rem 1rem;
|
||||
background: var(--code-bg, #f9fafb);
|
||||
border-bottom: 1px solid var(--border-color, #eee);
|
||||
}
|
||||
|
||||
.oa-method {
|
||||
color: #fff;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 4px;
|
||||
font-weight: 700;
|
||||
font-size: 0.75rem;
|
||||
min-width: 65px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.oa-path {
|
||||
font-weight: 600;
|
||||
font-family: var(--font-family-mono, monospace);
|
||||
color: var(--text-heading, #111);
|
||||
}
|
||||
|
||||
.oa-deprecated {
|
||||
font-size: 0.7rem;
|
||||
font-weight: 800;
|
||||
color: #ef4444;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.oa-summary {
|
||||
padding: 1rem 1rem 0;
|
||||
margin: 0;
|
||||
font-weight: 600;
|
||||
font-size: 1.1rem;
|
||||
color: var(--text-heading);
|
||||
}
|
||||
|
||||
.oa-description {
|
||||
padding: 0.5rem 1rem 1rem;
|
||||
margin: 0;
|
||||
color: var(--text-muted, #4b5563);
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.oa-operation h5 {
|
||||
margin: 1.5rem 1rem 0.5rem;
|
||||
font-size: 0.8rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--text-muted, #6b7280);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.oa-content-type {
|
||||
padding: 0 1rem;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.oa-content-type code {
|
||||
font-size: 0.8rem;
|
||||
background: var(--code-bg);
|
||||
padding: 0.2rem 0.4rem;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.oa-schema-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 1rem 0 0; /* Margin top only as requested */
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.oa-schema-table th, .oa-schema-table td {
|
||||
padding: 0.75rem 1rem;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--border-color, #eee);
|
||||
}
|
||||
|
||||
.oa-schema-table th {
|
||||
background: var(--code-bg, #f9fafb);
|
||||
font-weight: 600;
|
||||
color: var(--text-muted, #4b5563);
|
||||
}
|
||||
|
||||
.oa-required {
|
||||
color: #ef4444;
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
|
||||
.oa-type {
|
||||
color: #0891b2;
|
||||
font-family: var(--font-family-mono, monospace);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.oa-param-in {
|
||||
font-size: 0.75rem;
|
||||
background: var(--code-bg, #f3f4f6);
|
||||
padding: 0.125rem 0.375rem;
|
||||
border-radius: 4px;
|
||||
color: var(--text-muted, #4b5563);
|
||||
}
|
||||
|
||||
.oa-status-badge {
|
||||
padding: 0.125rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-weight: 600;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.oa-status-ok { background: #d1fae5; color: #065f46; }
|
||||
.oa-status-err { background: #fee2e2; color: #991b1b; }
|
||||
.oa-status-other { background: #f3f4f6; color: #4b5563; }
|
||||
|
||||
/* Dark Mode Overrides */
|
||||
:root[data-theme=dark] .oa-type {
|
||||
color: #22d3ee;
|
||||
}
|
||||
|
||||
:root[data-theme=dark] .oa-status-ok { background: #065f4633; color: #34d399; }
|
||||
:root[data-theme=dark] .oa-status-err { background: #991b1b33; color: #f87171; }
|
||||
:root[data-theme=dark] .oa-status-other { background: #27272a; color: #a1a1aa; }
|
||||
|
||||
.oa-error {
|
||||
padding: 1rem;
|
||||
background: #fef2f2;
|
||||
border: 1px solid #fee2e2;
|
||||
color: #991b1b;
|
||||
border-radius: 8px;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
:root[data-theme=dark] .oa-error {
|
||||
background: #991b1b22;
|
||||
border-color: #991b1b44;
|
||||
color: #f87171;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist", "**/*.test.ts"]
|
||||
}
|
||||
Reference in New Issue
Block a user