rapidoc
Version:
RapiDoc - Open API spec viewer with built in console
394 lines (363 loc) • 17 kB
text/typescript
import { html, css, nothing } from "lit";
import { customElement } from "lit/decorators.js";
import FontStyles from "../styles/font-styles.js";
import SchemaStyles from "../styles/schema-styles.js";
import BorderStyles from "../styles/border-styles.js";
import CustomStyles from "../styles/custom-styles.js";
import SchemaBaseElement from "./schema-base-element";
("schema-tree")
export default class SchemaTree extends SchemaBaseElement {
static get styles() {
return [
FontStyles,
SchemaStyles,
BorderStyles,
css`
.tree {
font-size: var(--font-size-small);
text-align: left;
direction: ltr;
line-height: calc(var(--font-size-small) + 6px);
& .tr:hover {
background-color: rgba(0, 0, 0, 0.15);
}
& .tr.expanded-descr {
max-height: none;
}
/* faint separators where descriptions wrap: every row in multiline mode, or around a row expanded by click */
&.expanded-all-descr .tr,
&.collapsed-all-descr .tr.expanded-descr {
border-bottom: 1px solid color-mix(in srgb, var(--border) 60%, transparent);
}
&.collapsed-all-descr .tr.expanded-descr {
box-shadow: 0 -1px 0 color-mix(in srgb, var(--border) 60%, transparent);
}
& .key {
max-width: 300px;
}
&.collapsed-all-descr .tr:not(.expanded-descr) {
overflow: hidden;
max-height: calc(var(--font-size-small) + 8px);
& .key {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
}
}
.tr {
&.expanded {
&:hover {
& > .td.key > .open-bracket,
& + .inside-bracket + .close-bracket {
color: var(--primary);
}
& + .inside-bracket {
border-left: 1px solid var(--muted-foreground);
}
}
& .open-bracket .open-bracket-collapsed {
display: none;
}
}
&.collapsed {
& + .inside-bracket,
& + .inside-bracket + .close-bracket {
overflow: hidden;
display: none;
}
& .open-bracket .open-bracket-expanded {
display: none;
}
}
}
/* Narrow schemas: a collapsed row holds its field line and the description's line under it (#1074) */
@container (width < 500px) {
.tree.collapsed-all-descr .tr:not(.expanded-descr) {
max-height: calc(2 * (var(--font-size-small) + 8px));
}
}
.inside-bracket {
&.xxx-of-option {
border-left: 1px solid transparent;
}
&.object,
&.array {
border-left: 1px dotted var(--border);
}
}
.open-bracket {
display: inline-block;
padding: 0 20px 0 0;
cursor: pointer;
border: 1px solid transparent;
border-radius: min(var(--radius), 0.5rem);
&:hover {
color: var(--primary);
border: 1px solid var(--border);
}
&:focus-visible {
outline: 2px solid var(--primary);
border-color: var(--primary);
}
}
.close-bracket {
display: inline-block;
font-family: var(--font-mono);
}
`,
CustomStyles,
];
}
render() {
if (!this.data) {
return html`
<div class="tree">
<span class="mono-font" style="color:var(--destructive)"> Schema not found </span>
</div>
`;
}
const rootType = this.data.kind === "primitive" ? this.data.type : this.data.kind || "";
const rootDescription = this.data.description || "";
return html` <div
class="tree ${this.schemaDescriptionExpanded === "true" ? "expanded-all-descr" : "collapsed-all-descr"}"
@click="${(e) => this.handleAllEvents(e)}"
@keydown="${(e) => this.handleKeyDown(e)}"
>
${this.renderToolbar(rootType)}
<span part="schema-description" class="m-markdown"> ${this.renderDescriptionHtml(rootDescription)}</span>
${this.renderAST(this.data)}
</div>`;
}
renderAST(node, parentType = "", schemaLevel = 0, indentLevel = 0) {
if (this.isFieldHidden(node)) {
return "";
}
const leftPadding = 12;
const minFieldColWidth = 400 - indentLevel * leftPadding;
const isExpanded = schemaLevel < this.schemaExpandLevel;
const readWriteBadge = this.getReadWriteBadge(node);
const readWriteTip = this.getReadWriteTip(node);
const nullMarker = node.nullable ? "null┃" : "";
const nullClass = node.nullable ? "nullable" : "";
const deprecatedIcon = this.renderDeprecatedIcon(node);
// 1. Array kind
if (node.kind === "array") {
const items = node.items;
if (!items) {
const fieldTitle = node.name
? `${node.name}${readWriteTip ? ` (${readWriteTip})` : ""}`
: "";
return html`
<div class="tr ${isExpanded ? "expanded" : "collapsed"} array ${nullClass}" title="${node.deprecated ? "Deprecated" : nothing}">
<div class="td key ${node.deprecated ? "deprecated" : ""}" style="min-width:${minFieldColWidth}px">
${deprecatedIcon}${node.name ? html`<span class="key-label" title="${fieldTitle}">${node.name}${node.required ? html`<span style="color:var(--destructive)">*</span>` : ""}${readWriteBadge}:</span>` : ""}
<span class="open-bracket array">${nullMarker}[ ]</span>
</div>
${this.renderDescrCell(node.description)}
</div>
`;
}
// If array of primitives
if (items.kind === "primitive") {
const itemType = items.format || items.contentMediaType || items.type || "string";
const dataTypeCss = this.getDataTypeCss(itemType);
const fieldTitle = node.name
? `${node.name}${readWriteTip ? ` (${readWriteTip})` : ""}`
: "";
return html`
<div class="tr primitive ${nullClass}" title="${node.deprecated || items.deprecated ? "Deprecated" : nothing}">
<div class="td key ${node.deprecated || items.deprecated ? "deprecated" : ""}" style="min-width:${minFieldColWidth}px">
${deprecatedIcon}${node.name ? html`<span class="key-label" title="${fieldTitle}">${node.name}${node.required ? html`<span style="color:var(--destructive)">*</span>` : ""}:</span>` : ""}
<span class="${dataTypeCss}" title="${readWriteTip || nothing}">${nullMarker}[${itemType}]${readWriteBadge}</span>
</div>
${this.renderDescrCell(node.description || items.description, items)}
</div>
`;
}
// If array of objects
if (items.kind === "object") {
const openBracket = html`<span
class="open-bracket array-of-object"
role="button"
tabindex="0"
aria-label="Toggle ${node.name || "object"}"
aria-expanded="${isExpanded ? "true" : "false"}"
>${nullMarker}<span class="open-bracket-expanded">[{</span><span class="open-bracket-collapsed">[{...}]</span></span
>`;
const closeBracket = "}]";
const fieldTitle = node.name
? `${node.name}${readWriteTip ? ` (${readWriteTip})` : ""}`
: "";
return html`
<div
class="tr ${isExpanded ? "expanded" : "collapsed"} object ${nullClass}"
title="${node.deprecated || items.deprecated ? "Deprecated" : nothing}"
>
<div class="td key ${node.deprecated || items.deprecated ? "deprecated" : ""}" style="min-width:${minFieldColWidth}px">
${deprecatedIcon}${node.name ? html`<span class="key-label" title="${fieldTitle}">${node.name}${node.required ? html`<span style="color:var(--destructive)">*</span>` : ""}${readWriteBadge}:</span>` : ""}
${openBracket}
</div>
${this.renderDescrCell(node.description || items.description)}
</div>
<div class="inside-bracket object" style="padding-left:${leftPadding}px;">
${items.properties?.map((p) => this.renderAST(p, "object", schemaLevel + 1, indentLevel + 1))}
${items.patternProperties?.map((p) => this.renderAST(p, "object", schemaLevel + 1, indentLevel + 1))}
${items.additionalProperties ? this.renderAST(items.additionalProperties, "object", schemaLevel + 1, indentLevel + 1) : ""}
${items.unions?.map((u) => this.renderAST(u, "object", schemaLevel + 1, indentLevel + 1))}
</div>
<div class="close-bracket">${closeBracket}</div>
`;
}
// If array of arrays
if (items.kind === "array") {
const arrType = node.arrayType !== "object" ? node.arrayType : "";
const openBracket = html`<span
class="open-bracket array-of-array"
data-array-type="${arrType}"
role="button"
tabindex="0"
aria-label="Toggle ${node.name || "array"}"
aria-expanded="${isExpanded ? "true" : "false"}"
>${nullMarker}<span class="open-bracket-expanded">[[ ${arrType} </span><span class="open-bracket-collapsed">[[...]]</span></span
>`;
const closeBracket = "]]";
const fieldTitle = node.name
? `${node.name}${readWriteTip ? ` (${readWriteTip})` : ""}`
: "";
return html`
<div class="tr ${isExpanded ? "expanded" : "collapsed"} array ${nullClass}" title="${node.deprecated ? "Deprecated" : nothing}">
<div class="td key ${node.deprecated ? "deprecated" : ""}" style="min-width:${minFieldColWidth}px">
${deprecatedIcon}${node.name ? html`<span class="key-label" title="${fieldTitle}">${node.name}${node.required ? html`<span style="color:var(--destructive)">*</span>` : ""}${readWriteBadge}:</span>` : ""}
${openBracket}
</div>
${this.renderDescrCell(node.description)}
</div>
<div class="inside-bracket array" style="padding-left:${leftPadding}px;">
${this.renderAST(items, "array", schemaLevel + 1, indentLevel + 1)}
</div>
<div class="close-bracket">${closeBracket}</div>
`;
}
// If array of unions or others
const fieldTitle = node.name ? `${node.name}${readWriteTip ? ` (${readWriteTip})` : ""}` : "";
return html`
<div class="tr ${isExpanded ? "expanded" : "collapsed"} array ${nullClass}" title="${node.deprecated ? "Deprecated" : nothing}">
<div class="td key ${node.deprecated ? "deprecated" : ""}" style="min-width:${minFieldColWidth}px">
${deprecatedIcon}${node.name ? html`<span class="key-label" title="${fieldTitle}">${node.name}${node.required ? html`<span style="color:var(--destructive)">*</span>` : ""}${readWriteBadge}:</span>` : ""}
<span
class="open-bracket array"
role="button"
tabindex="0"
aria-label="Toggle ${node.name || "array"}"
aria-expanded="${isExpanded ? "true" : "false"}"
>${nullMarker}<span class="open-bracket-expanded">[</span><span class="open-bracket-collapsed">[...]</span></span
>
</div>
${this.renderDescrCell(node.description)}
</div>
<div class="inside-bracket array" style="padding-left:${leftPadding}px;">
${this.renderAST(items, "array", schemaLevel + 1, indentLevel + 1)}
</div>
<div class="close-bracket">]</div>
`;
}
// 2. Object kind
if (node.kind === "object") {
const openBracket = html`<span
class="open-bracket object"
role="button"
tabindex="0"
aria-label="Toggle ${node.name || "object"}"
aria-expanded="${isExpanded ? "true" : "false"}"
>${nullMarker}<span class="open-bracket-expanded">{</span><span class="open-bracket-collapsed">{...}</span></span
>`;
const closeBracket = "}";
const fieldTitle = node.name ? `${node.name}${readWriteTip ? ` (${readWriteTip})` : ""}` : "";
return html`
<div class="tr ${isExpanded ? "expanded" : "collapsed"} object ${nullClass}" title="${node.deprecated ? "Deprecated" : nothing}">
<div class="td key ${node.deprecated ? "deprecated" : ""}" style="min-width:${minFieldColWidth}px">
${deprecatedIcon}${node.name ? html`<span class="key-label" title="${fieldTitle}">${node.name}${node.required ? html`<span style="color:var(--destructive)">*</span>` : ""}${readWriteBadge}:</span>` : ""}
${openBracket}
</div>
${this.renderDescrCell(node.description)}
</div>
<div class="inside-bracket object" style="padding-left:${leftPadding}px;">
${node.properties?.map((p) => this.renderAST(p, "object", schemaLevel + 1, indentLevel + 1))}
${node.patternProperties?.map((p) => this.renderAST(p, "object", schemaLevel + 1, indentLevel + 1))}
${node.additionalProperties ? this.renderAST(node.additionalProperties, "object", schemaLevel + 1, indentLevel + 1) : ""}
${node.unions?.map((u) => this.renderAST(u, "object", schemaLevel + 1, indentLevel + 1))}
</div>
<div class="close-bracket">${closeBracket}</div>
`;
}
// 3. Union kind (oneOf / anyOf)
if (node.kind === "union") {
const opLabel =
(node.operator === "anyOf" ? "ANY OF" : "ONE OF") + (node.suffix ? ` ${node.suffix}` : "");
const newIndent = parentType === "xxx-of-option" ? indentLevel : indentLevel + 1;
// Options are already labelled by their OPT row, so only property names are shown
const showName = node.name && parentType !== "xxx-of-option";
// A named union puts its label and options one level inside the property row
const bodyFieldColWidth = showName ? minFieldColWidth - leftPadding : minFieldColWidth;
const unionBody = html`
<div class="tr expanded xxx-of-option">
<div class="td key" style="min-width:${bodyFieldColWidth}px">
<span class="key-label xxx-of-key">${opLabel}</span>
</div>
${showName ? "" : this.renderDescrCell(node.description)}
</div>
<div class="inside-bracket xxx-of-option" style="padding-left:0px;">
${node.options?.map(
(opt, i) => html`
<div>
<div class="tr expanded xxx-of-option">
<div class="td key" style="min-width:${bodyFieldColWidth}px">
<span class="key-label xxx-of-key">OPT ${opt.optionIndex || i + 1}</span>
${opt.optionTitle ? html`<span class="xxx-of-descr">${opt.optionTitle}</span>` : ""}
</div>
</div>
${this.renderAST(opt, "xxx-of-option", schemaLevel, newIndent)}
</div>
`,
)}
</div>
`;
return html`
${node.properties?.map((p) => this.renderAST(p, "object", schemaLevel, indentLevel))}
${
showName
? html`
<div class="tr expanded xxx-of-prop">
<div class="td key" style="min-width:${minFieldColWidth}px">
<span class="key-label" title="${node.name}"
>${node.name}${node.required ? html`<span style="color:var(--destructive)">*</span>` : ""}:</span
>
</div>
${this.renderDescrCell(node.description)}
</div>
<div class="inside-bracket xxx-of-option" style="padding-left:${leftPadding}px;">${unionBody}</div>
`
: unionBody
}
`;
}
// 4. Primitive kind
if (node.kind === "primitive") {
const dataTypeCss = this.getDataTypeCss(node.type);
const fieldTitle = node.name ? `${node.name}${readWriteTip ? ` (${readWriteTip})` : ""}` : "";
return html`
<div class="tr primitive" title="${node.deprecated ? "Deprecated" : nothing}">
<div class="td key ${node.deprecated ? "deprecated" : ""}" style="min-width:${minFieldColWidth}px">
${deprecatedIcon}${node.name ? html`<span class="key-label" title="${fieldTitle}">${node.name}${node.required ? html`<span style="color:var(--destructive);">*</span>` : ""}:</span>` : ""}
<span class="${dataTypeCss}" title="${readWriteTip || nothing}"
>${parentType === "array" ? `[${node.type}]` : node.type}${readWriteBadge}</span
>
</div>
${this.renderDescrCell(node.description, node, node.title)}
</div>
`;
}
return "";
}
}