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.handleAllEvents(e)}"
@keydown="${(e) => this.handleKeyDown(e)}"
>
${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 "";
}
}