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 CustomStyles from "../styles/custom-styles.js"; import SchemaBaseElement from "./schema-base-element"; @customElement("schema-table") export default class SchemaTable extends SchemaBaseElement { static get styles() { return [ FontStyles, SchemaStyles, css` .table { font-size: var(--font-size-small); text-align: left; line-height: calc(var(--font-size-small) + 6px); & .table-container { border: 1px solid var(--border); /* Card surface, same as .param-list */ background: color-mix(in srgb, var(--card) 80%, transparent); overflow-x: auto; max-width: 100%; } & .table-header { display: flex; background: var(--card); padding: 8px 4px; border-bottom: 1px solid var(--border); & .td { font-family: var(--font-sans); font-weight: bold; color: var(--foreground); } } & .tr { width: calc(100% - 5px); padding: 0 0 0 5px; border-bottom: 1px dotted var(--border); &.expanded { & .obj-toggle::after { content: '-'; } & + .object-body { display: block; } } &.collapsed { & .obj-toggle::after { content: '+'; } & + .object-body { display: none; } } } & .td { padding: 4px 0; } & .key { width: var(--table-schema-key-width); text-overflow: var(--table-schema-key-text-overflow); white-space: var(--table-schema-key-whitespace); overflow: hidden; & .key-label { font-size: var(--font-size-mono); } &.deprecated .key-label { color: var(--destructive); } } & .key-type { white-space: normal; width: 150px; } &.collapsed-all-descr .tr:not(.expanded-descr) { overflow: hidden; max-height: calc(var(--font-size-small) + 16px); } } .obj-toggle { background: transparent; border: 1px solid transparent; padding: 0; border-radius: 2px; display: inline-flex; align-items: center; justify-content: center; margin-left: -16px; margin-top: -2px; color: var(--primary); cursor: pointer; font-size: 14px; font-family: var(--font-mono); line-height: 12px; width: 14px; height: 14px; min-width: 14px; text-align: center; user-select: none; box-sizing: border-box; vertical-align: middle; &:hover { border-color: var(--primary); } &:focus-visible { outline: 2px solid var(--primary); border-color: var(--primary); } } @supports (grid-template-columns: subgrid) { .table { & .table-container { display: grid; grid-template-columns: minmax(var(--table-schema-key-width), auto) minmax(150px, auto) 1fr; } & .table-header, & .tr { display: grid; grid-column: 1 / -1; grid-template-columns: subgrid; width: 100%; padding: 0 4px; box-sizing: border-box; & .key, & .key-type { width: auto; } & .key-descr { min-width: 0; } } & .object-body { display: contents; } & .tr.collapsed + .object-body { display: none; } & .tr.expanded + .object-body { display: contents; } } } /* Narrow schemas: the description spans the row, under the field and type, with no column of its own (#1074) */ @container (width < 500px) { .table { /* no column for descriptions, and field and type share the width instead of scrolling sideways */ & .table-container { grid-template-columns: minmax(0, 1fr) minmax(0, max-content) 0; } & .tr > .key-descr { grid-column: 1 / -1; } & .table-header > .key-descr { display: none; } &.collapsed-all-descr .tr:not(.expanded-descr) { max-height: calc(2 * (var(--font-size-small) + 16px)); } } } `, CustomStyles, ]; } render() { if (!this.data) { return ""; } 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)}
Field
Type
Description
${this.renderAST(this.data)}
`; } renderAST(node, parentType = "", schemaLevel = 0, indentLevel = 0) { if (this.isFieldHidden(node)) { return ""; } const leftPadding = 16 * (indentLevel + 1); const isExpanded = schemaLevel < this.schemaExpandLevel; const nullClass = node.nullable ? "nullable" : ""; const deprecatedIcon = this.renderDeprecatedIcon(node); const readWriteBadge = this.getReadWriteBadge(node); const readWriteTip = this.getReadWriteTip(node); // 1. Array kind if (node.kind === "array") { const items = node.items; if (!items) { const typeLabel = node.dataTypeLabel || (node.nullable ? "array ┃ null" : "array"); return html`
${deprecatedIcon}${node.name || ""} ${node.required ? html`*` : ""}
${typeLabel}${readWriteBadge}
${this.renderDescrCell(node.description)}
`; } if (items.kind === "primitive") { const itemType = items.format || items.contentMediaType || items.type || "string"; const dataTypeCss = this.getDataTypeCss(itemType); return html`
${deprecatedIcon}${node.name ? html`${node.name}` : ""} ${node.required ? html`*` : ""}
[${itemType}]${node.nullable ? " ┃ null" : ""}${readWriteBadge}
${this.renderDescrCell(node.description || items.description, items)}
`; } let arrLabel = "array"; if (items.kind === "object") { arrLabel = "array of object"; } else if (items.kind === "array") { arrLabel = `array of array ${node.arrayType ? `of ${node.arrayType}` : ""}`.trim(); } else if (items.kind === "union") { arrLabel = "array of union"; } if (node.nullable) { arrLabel += " ┃ null"; } const hasChildren = items.kind === "object" ? !!( (items.properties && items.properties.length > 0) || (items.patternProperties && items.patternProperties.length > 0) || items.additionalProperties || (items.unions && items.unions.length > 0) ) : true; return html`
${ node.name && hasChildren ? html`` : "" } ${deprecatedIcon}${node.name || ""} ${node.required ? html`*` : ""}
${arrLabel}${readWriteBadge}
${this.renderDescrCell(node.description || items.description)}
${ hasChildren ? html`
${ items.kind === "object" ? html` ${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))} ` : this.renderAST(items, "array", schemaLevel + 1, indentLevel + 1) }
` : "" } `; } // 2. Object kind if (node.kind === "object") { const typeLabel = node.dataTypeLabel || (node.nullable ? "object ┃ null" : "object"); const hasChildren = !!( (node.properties && node.properties.length > 0) || (node.patternProperties && node.patternProperties.length > 0) || node.additionalProperties || (node.unions && node.unions.length > 0) ); return html` ${ node.name ? html`
${ hasChildren ? html`` : "" } ${deprecatedIcon}${node.name} ${node.required ? html`*` : ""}
${typeLabel}${readWriteBadge}
${this.renderDescrCell(node.description)}
` : "" } ${ hasChildren ? html`
${node.properties?.map((p) => this.renderAST(p, "object", schemaLevel + 1, indentLevel + (node.name ? 1 : 0)))} ${node.patternProperties?.map((p) => this.renderAST(p, "object", schemaLevel + 1, indentLevel + (node.name ? 1 : 0)))} ${node.additionalProperties ? this.renderAST(node.additionalProperties, "object", schemaLevel + 1, indentLevel + (node.name ? 1 : 0)) : ""} ${node.unions?.map((u) => this.renderAST(u, "object", schemaLevel + 1, indentLevel + (node.name ? 1 : 0)))}
` : "" } `; } // 3. Union kind (oneOf / anyOf) if (node.kind === "union") { const opLabel = (node.operator === "anyOf" ? "ANY OF" : "ONE OF") + (node.suffix ? ` ${node.suffix}` : ""); // Options are already labelled by their OPT row, so only property names are shown const showName = node.name && parentType !== "xxx-of-option"; return html` ${node.properties?.map((p) => this.renderAST(p, "object", schemaLevel, indentLevel))}
${ showName ? html`
${node.name} ${node.required ? html`*` : ""}
${opLabel}
` : html`
${opLabel}
` } ${this.renderDescrCell(node.description)}
${node.options?.map((opt, i) => opt.kind === "primitive" ? // A primitive option fits on a single row: the badge, then its type and description html`
${this.renderAST(opt, "xxx-of-option", schemaLevel + 1, indentLevel + 1)}
` : html`
OPT ${opt.optionIndex || i + 1} ${ opt.optionTitle ? html`${opt.optionTitle}` : "" }
${this.renderAST(opt, "xxx-of-option", schemaLevel + 1, indentLevel + 1)}
`, )}
`; } // 4. Primitive kind if (node.kind === "primitive") { const dataTypeCss = this.getDataTypeCss(node.type); // Primitive option of a oneOf / anyOf: the OPT n badge (and the title) takes the place of the name const isOption = parentType === "xxx-of-option" && !!node.optionIndex; return html`
${ isOption ? html`OPT ${node.optionIndex}${ node.optionTitle ? html`${node.optionTitle}` : "" }` : "" }${deprecatedIcon}${ node.name && !isOption ? html`${node.name}` : "" } ${node.required ? html`*` : ""}
${parentType === "array" ? `[${node.type}]` : node.type}${readWriteBadge}
${this.renderDescrCell(node.description, node, node.title)}
`; } return ""; } }