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`
Schema not found
`; } const rootType = this.data.kind === "primitive" ? this.data.type : this.data.kind || ""; const rootDescription = this.data.description || ""; return html`
${this.renderToolbar(rootType)} ${this.renderDescriptionHtml(rootDescription)} ${this.renderAST(this.data)}
`; } 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`
${deprecatedIcon}${node.name ? html`${node.name}${node.required ? html`*` : ""}${readWriteBadge}:` : ""} ${nullMarker}[ ]
${this.renderDescrCell(node.description)}
`; } // 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`
${deprecatedIcon}${node.name ? html`${node.name}${node.required ? html`*` : ""}:` : ""} ${nullMarker}[${itemType}]${readWriteBadge}
${this.renderDescrCell(node.description || items.description, items)}
`; } // If array of objects if (items.kind === "object") { const openBracket = html`${nullMarker}[{[{...}]`; const closeBracket = "}]"; const fieldTitle = node.name ? `${node.name}${readWriteTip ? ` (${readWriteTip})` : ""}` : ""; return html`
${deprecatedIcon}${node.name ? html`${node.name}${node.required ? html`*` : ""}${readWriteBadge}:` : ""} ${openBracket}
${this.renderDescrCell(node.description || items.description)}
${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))}
${closeBracket}
`; } // If array of arrays if (items.kind === "array") { const arrType = node.arrayType !== "object" ? node.arrayType : ""; const openBracket = html`${nullMarker}[[ ${arrType} [[...]]`; const closeBracket = "]]"; const fieldTitle = node.name ? `${node.name}${readWriteTip ? ` (${readWriteTip})` : ""}` : ""; return html`
${deprecatedIcon}${node.name ? html`${node.name}${node.required ? html`*` : ""}${readWriteBadge}:` : ""} ${openBracket}
${this.renderDescrCell(node.description)}
${this.renderAST(items, "array", schemaLevel + 1, indentLevel + 1)}
${closeBracket}
`; } // If array of unions or others const fieldTitle = node.name ? `${node.name}${readWriteTip ? ` (${readWriteTip})` : ""}` : ""; return html`
${deprecatedIcon}${node.name ? html`${node.name}${node.required ? html`*` : ""}${readWriteBadge}:` : ""} ${nullMarker}[[...]
${this.renderDescrCell(node.description)}
${this.renderAST(items, "array", schemaLevel + 1, indentLevel + 1)}
]
`; } // 2. Object kind if (node.kind === "object") { const openBracket = html`${nullMarker}{{...}`; const closeBracket = "}"; const fieldTitle = node.name ? `${node.name}${readWriteTip ? ` (${readWriteTip})` : ""}` : ""; return html`
${deprecatedIcon}${node.name ? html`${node.name}${node.required ? html`*` : ""}${readWriteBadge}:` : ""} ${openBracket}
${this.renderDescrCell(node.description)}
${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))}
${closeBracket}
`; } // 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`
${opLabel}
${showName ? "" : this.renderDescrCell(node.description)}
${node.options?.map( (opt, i) => html`
OPT ${opt.optionIndex || i + 1} ${opt.optionTitle ? html`${opt.optionTitle}` : ""}
${this.renderAST(opt, "xxx-of-option", schemaLevel, newIndent)}
`, )}
`; return html` ${node.properties?.map((p) => this.renderAST(p, "object", schemaLevel, indentLevel))} ${ showName ? html`
${node.name}${node.required ? html`*` : ""}:
${this.renderDescrCell(node.description)}
${unionBody}
` : unionBody } `; } // 4. Primitive kind if (node.kind === "primitive") { const dataTypeCss = this.getDataTypeCss(node.type); const fieldTitle = node.name ? `${node.name}${readWriteTip ? ` (${readWriteTip})` : ""}` : ""; return html`
${deprecatedIcon}${node.name ? html`${node.name}${node.required ? html`*` : ""}:` : ""} ${parentType === "array" ? `[${node.type}]` : node.type}${readWriteBadge}
${this.renderDescrCell(node.description, node, node.title)}
`; } return ""; } }