rapidoc
Version:
RapiDoc - Open API spec viewer with built in console
271 lines (258 loc) • 13.6 kB
JavaScript
/**
* Renders all API operations in an expanded continuous documentation layout grouped by tags.
*/
import { fixRenderedAnchorLinks } from "~/utils/markdown-utils";
import { html, nothing } from "lit";
import { unsafeHTML } from "lit/directives/unsafe-html.js";
import { sanitizeHTML } from "../utils/sanitize.js";
import { marked } from "marked";
import Slugger from "github-slugger";
import { rapidocApiKey } from "~/utils/common-utils";
import { pathSecurityTemplate } from "~/templates/security-scheme-template";
import codeSamplesTemplate from "~/templates/code-samples-template";
import callbackTemplate from "~/templates/callback-template";
import "~/components/api-request";
import "~/components/api-response";
function headingRenderer(tagElementId) {
const slugger = new Slugger();
const renderer = new marked.Renderer();
renderer.heading = ({ text, depth }) =>
`<h${depth} class="observe-me" id="${tagElementId}--${slugger.slug(text)}">${text}</h${depth}>`;
fixRenderedAnchorLinks(renderer);
return renderer;
}
export function expandedEndpointBodyTemplate(
path,
tagName = "",
tagDescription = "",
isFirst = false,
) {
const acceptContentTypes = new Set();
for (const respStatus in path.responses) {
for (const acceptContentType in path.responses[respStatus]?.content) {
acceptContentTypes.add(acceptContentType.trim());
}
}
const accept = [...acceptContentTypes].join(", ");
// Filter API Keys that are non-empty and are applicable to the the path
const nonEmptyApiKeys =
this.resolvedSpec.securitySchemes.filter(
(v) => v.finalKeyValue && path.security?.some((ps) => v.securitySchemeId in ps),
) || [];
// If a RapiDoc API Key is specified on the element and its value is not hyphen(-) then include it for all paths
const rapiDocApiKey = this.resolvedSpec.securitySchemes.find(
(v) => v.securitySchemeId === rapidocApiKey && v.value !== "-",
);
if (rapiDocApiKey) {
nonEmptyApiKeys.push(rapiDocApiKey);
}
const codeSampleTabPanel = path.xCodeSamples
? codeSamplesTemplate.call(this, path.xCodeSamples)
: "";
return html`
${this.renderStyle === "read" && !isFirst ? html`<div class="divider" part="operation-divider"></div>` : ""}
<div
class="expanded-endpoint-body observe-me ${path.method} ${path.deprecated ? "deprecated" : ""} "
part="section-operation ${path.elementId}"
id="${path.elementId}"
data-first-operation-in-tag="${isFirst ? "true" : nothing}"
>
${
this.renderStyle === "focused" && tagName
? html`
<div class="tag-container" part="section-operation-tag" style="margin-bottom: 16px;">
<h2 class="rapidoc-heading upper" part="heading heading-2" style="font-size:var(--h3-font-size); margin:0;">${tagName}</h2>
${
isFirst && tagDescription
? html`
<div
class="tag-description regular-font"
part="section-tag-description"
style="margin-top:12px; padding:12px 14px; background:var(--code-bg); border-radius:min(var(--radius), 0.5rem); border:1px solid var(--border);"
>
<div class="m-markdown">${unsafeHTML(sanitizeHTML(marked(tagDescription)))}</div>
</div>
`
: ""
}
</div>
`
: ""
}
${path.deprecated ? html`<div class="bold-text red-text">DEPRECATED</div>` : ""}
${html` ${
path.xBadges && path.xBadges?.length > 0
? html`
<div style="display:flex; flex-wrap:wrap; margin-bottom: -24px; font-size: var(--font-size-small);">
${path.xBadges.map((v) =>
v.color === "none"
? ""
: html`<span
style="margin:1px; margin-right:5px; padding:1px 8px; font-weight:bold; border-radius:var(--radius); background: color-mix(in srgb, var(--${v.color}) 10%, transparent); color:var(--${v.color}); border:1px solid var(--${v.color})"
>${v.label}</span
>`,
)}
</div>
`
: ""
}
<h3 class="rapidoc-heading operation-summary" part="section-operation-summary heading heading-3">
${path.shortSummary || `${path.method.toUpperCase()} ${path.path}`}
</h3>
${
path.isWebhook
? html`<span
part="section-operation-webhook"
style="color:var(--primary); font-weight:bold; font-size: var(--font-size-regular);"
>
WEBHOOK
</span>`
: html`
<div
part="section-operation-webhook-method"
class="mono-font regular-font-size"
style="text-align:left; direction:ltr; padding: 8px 0; color:var(--muted-foreground)"
>
<span part="label-operation-method" class="regular-font upper method-fg bold-text ${path.method}">${path.method}</span>
<span style="overflow-wrap: break-word;" part="label-operation-path">${path.path}</span>
</div>
`
}
${pathSecurityTemplate.call(this, path.security, path.elementId)}
<slot name="${path.elementId}"></slot>`}
${path.description ? html`<div class="m-markdown">${unsafeHTML(sanitizeHTML(marked(path.description)))}</div>` : ""}
${
path.externalDocs?.url || path.externalDocs?.description
? html`<div class="external-docs-box">
<div class="m-markdown">${unsafeHTML(sanitizeHTML(marked(path.externalDocs?.description || "")))}</div>
${
path.externalDocs?.url
? html`<a class="external-docs-link" href="${path.externalDocs?.url}" target="_blank">
${path.externalDocs?.url}
<span class="external-docs-arrow">⇲</span>
</a>`
: ""
}
</div>`
: ""
}
${codeSampleTabPanel}
<div class="req-resp-container expanded-req-resp-container ${this.layout}-layout" part="section-req-resp-container">
<div class="request-panel view-mode-request ${this.renderStyle}-mode ${this.layout}-layout" part="section-request-panel">
<api-request
.config="${this.config}"
class="${this.renderStyle}-mode ${this.layout}-layout"
style="width:100%;"
webhook="${path.isWebhook}"
method="${path.method}"
path="${path.path}"
.security="${path.security}"
.parameters="${path.parameters}"
.request_body="${path.requestBody}"
.api_keys="${nonEmptyApiKeys}"
.servers="${path.servers}"
server-url="${path.servers?.[0]?.url || this.selectedServer?.computedUrl}"
fill-request-fields-with-example="${this.fillRequestFieldsWithExample}"
allow-try="${this.allowTry}"
show-curl-before-try="${this.showCurlBeforeTry}"
accept="${accept}"
render-style="${this.renderStyle}"
schema-style="${this.schemaStyle}"
active-schema-tab="${this.defaultSchemaTab}"
schema-expand-level="${this.schemaExpandLevel}"
schema-description-expanded="${this.schemaDescriptionExpanded}"
allow-schema-description-expand-toggle="${this.allowSchemaDescriptionExpandToggle}"
schema-hide-read-only="${this.schemaHideReadOnly === "never" ? "false" : path.isWebhook ? "false" : "true"}"
schema-hide-write-only="${this.schemaHideWriteOnly === "never" ? "false" : path.isWebhook ? "true" : "false"}"
fetch-credentials="${this.fetchCredentials}"
exportparts="section-request-title:section-request-title, heading:heading, heading-4:heading-4, heading-5:heading-5, heading-6:heading-6,
wrap-request-btn:wrap-request-btn, btn:btn, btn-fill:btn-fill, btn-outline:btn-outline, btn-try:btn-try, btn-clear:btn-clear, btn-fill-example:btn-fill-example, btn-clear-resp:btn-clear-resp,
tab-panel:tab-panel, tab-btn:tab-btn, tab-btn-row:tab-btn-row, tab-content:tab-content,
file-input:file-input, textbox:textbox, textbox-param:textbox-param, textarea:textarea, textarea-param:textarea-param,
anchor:anchor, anchor-param-example:anchor-param-example, anchor-param-constraint:anchor-param-constraint, param-list:param-list, param:param, param-label:param-label, param-name:param-name, param-type:param-type, param-input:param-input, param-constraint:param-constraint, param-constraint-label:param-constraint-label, param-details:param-details, param-example-list:param-example-list, value-list:value-list, value-list-label:value-list-label, value-list-items:value-list-items, schema-description:schema-description, schema-multiline-toggle:schema-multiline-toggle"
>
</api-request>
${path.callbacks ? callbackTemplate.call(this, path.callbacks) : ""}
</div>
<api-response
.config="${this.config}"
class="response-panel ${this.renderStyle}-mode ${this.layout}-layout"
style="width:100%;"
webhook="${path.isWebhook}"
.responses="${path.responses}"
render-style="${this.renderStyle}"
schema-style="${this.schemaStyle}"
active-schema-tab="${this.defaultSchemaTab}"
schema-expand-level="${this.schemaExpandLevel}"
schema-description-expanded="${this.schemaDescriptionExpanded}"
allow-schema-description-expand-toggle="${this.allowSchemaDescriptionExpandToggle}"
schema-hide-read-only="${this.schemaHideReadOnly === "never" ? "false" : path.isWebhook ? "true" : "false"}"
schema-hide-write-only="${this.schemaHideWriteOnly === "never" ? "false" : path.isWebhook ? "false" : "true"}"
selected-status="${Object.keys(path.responses || {})[0] || ""}"
exportparts="section-response-title:section-response-title, heading:heading, heading-4:heading-4, heading-5:heading-5, heading-6:heading-6,
btn:btn, btn-response-status:btn-response-status, btn-selected-response-status:btn-selected-response-status, btn-fill:btn-fill, btn-outline:btn-outline, btn-copy:btn-copy,
tab-panel:tab-panel, tab-btn:tab-btn, tab-btn-row:tab-btn-row, tab-content:tab-content,
schema-description:schema-description, schema-multiline-toggle:schema-multiline-toggle, param-list:param-list, param:param, param-label:param-label, param-name:param-name, param-type:param-type, param-constraint:param-constraint, param-constraint-label:param-constraint-label, param-details:param-details, param-example-list:param-example-list, value-list:value-list, value-list-label:value-list-label, value-list-items:value-list-items"
>
</api-response>
</div>
</div>
`;
}
export default function expandedEndpointTemplate() {
if (!this.resolvedSpec) {
return "";
}
const renderTagSections = (tag) => html`
<section id="${tag.elementId}" part="section-tag" class="regular-font section-gap--read-mode observe-me read-mode-tag">
<h2 class="rapidoc-heading title tag" part="section-tag-title label-tag-title heading heading-2">${tag.displayName || tag.name}</h2>
<slot name="${tag.elementId}"></slot>
${unsafeHTML(`
<div class="m-markdown regular-font">
${sanitizeHTML(
marked(
tag.description || "",
this.infoDescriptionHeadingsInNavBar === "true"
? { renderer: headingRenderer(tag.elementId) }
: undefined,
),
)}
</div>`)}
</section>
${
tag.paths.length > 0
? html`<section class="regular-font section-gap--read-mode" part="section-operations-in-tag">
${tag.paths.map((path, idx) => expandedEndpointBodyTemplate.call(this, path, tag.name, tag.description, idx === 0))}
</section>`
: ""
}
`;
if (this.resolvedSpec.tagGroups && this.resolvedSpec.tagGroups.length > 0) {
const assignedTagNames = new Set();
this.resolvedSpec.tagGroups.forEach((group) => {
(group.tags || []).forEach((tag) => assignedTagNames.add(tag.name.toLowerCase()));
});
const remainingTags = (this.resolvedSpec.tags || []).filter(
(tag) => !assignedTagNames.has(tag.name.toLowerCase()),
);
return html`
${this.resolvedSpec.tagGroups.map(
(group) => html`
<section id="${group.elementId}" part="section-tag-group" class="regular-font section-gap--read-mode observe-me read-mode-group">
<h2 class="rapidoc-heading title tag-group" part="section-tag-group-title heading heading-2">
${group.displayName || group.name}
</h2>
${
group.description
? html`<div class="m-markdown tag-group-description">${unsafeHTML(sanitizeHTML(marked(group.description)))}</div>`
: ""
}
</section>
${group.tags.map((tag) => renderTagSections(tag))}
`,
)}
${remainingTags.map((tag) => renderTagSections(tag))}
`;
}
return html`${this.resolvedSpec.tags.map((tag) => renderTagSections(tag))}`;
}