UNPKG

rapidoc

Version:

RapiDoc - Open API spec viewer with built in console

394 lines (363 loc) • 17 kB
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"; @customElement("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 ""; } }