rapidoc
Version:
RapiDoc - Open API spec viewer with built in console
382 lines (370 loc) • 17.2 kB
JavaScript
import { LitElement, html, css } from 'lit';
import { unsafeHTML } from 'lit/directives/unsafe-html.js'; // eslint-disable-line import/extensions
import { marked } from 'marked';
import FontStyles from '~/styles/font-styles';
import SchemaStyles from '~/styles/schema-styles';
import BorderStyles from '~/styles/border-styles';
import CustomStyles from '~/styles/custom-styles';
export default class SchemaTree extends LitElement {
static get properties() {
return {
data: { type: Object },
schemaExpandLevel: { type: Number, attribute: 'schema-expand-level' },
schemaDescriptionExpanded: { type: String, attribute: 'schema-description-expanded' },
allowSchemaDescriptionExpandToggle: { type: String, attribute: 'allow-schema-description-expand-toggle' },
schemaHideReadOnly: { type: String, attribute: 'schema-hide-read-only' },
schemaHideWriteOnly: { type: String, attribute: 'schema-hide-write-only' },
};
}
connectedCallback() {
super.connectedCallback();
if (!this.schemaExpandLevel || this.schemaExpandLevel < 1) { this.schemaExpandLevel = 99999; }
if (!this.schemaDescriptionExpanded || !'true false'.includes(this.schemaDescriptionExpanded)) { this.schemaDescriptionExpanded = 'false'; }
if (!this.schemaHideReadOnly || !'true false'.includes(this.schemaHideReadOnly)) { this.schemaHideReadOnly = 'true'; }
if (!this.schemaHideWriteOnly || !'true false'.includes(this.schemaHideWriteOnly)) { this.schemaHideWriteOnly = 'true'; }
}
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);
}
.tree .tr:hover{
background-color:var(--hover-color);
}
.collapsed-all-descr .tr:not(.expanded-descr) {
overflow: hidden;
max-height:calc(var(--font-size-small) + 8px);
}
.tree .key {
max-width: 300px;
}
.tr.expanded:hover > .td.key > .open-bracket {
color: var(--primary-color);
}
.tr.expanded:hover + .inside-bracket {
border-left: 1px solid var(--fg3);
}
.tr.expanded:hover + .inside-bracket + .close-bracket {
color: var(--primary-color);
}
.inside-bracket.xxx-of-option {
border-left: 1px solid transparent;
}
.open-bracket{
display:inline-block;
padding: 0 20px 0 0;
cursor:pointer;
border: 1px solid transparent;
border-radius:3px;
}
.open-bracket:hover {
color:var(--primary-color);
background-color:var(--hover-color);
border: 1px solid var(--border-color);
}
.close-bracket{
display:inline-block;
font-family: var(--font-mono);
}
.tr.collapsed + .inside-bracket,
.tr.collapsed + .inside-bracket + .close-bracket{
overflow: hidden;
display:none;
}
.inside-bracket.object,
.inside-bracket.array {
border-left: 1px dotted var(--border-color);
}`,
CustomStyles,
];
}
/* eslint-disable indent */
render() {
return html`
<div class="tree ${this.schemaDescriptionExpanded === 'true' ? 'expanded-all-descr' : 'collapsed-all-descr'}" @click="${(e) => this.handleAllEvents(e)}">
<div class="toolbar">
<div class="toolbar-item schema-root-type ${this.data?.['::type'] || ''} "> ${this.data?.['::type'] || ''} </div>
${this.allowSchemaDescriptionExpandToggle === 'true'
? html`
<div style="flex:1"></div>
<div part="schema-toolbar-item schema-multiline-toggle" class='toolbar-item schema-multiline-toggle'>
${this.schemaDescriptionExpanded === 'true' ? 'Single line description' : 'Multiline description'}
</div>`
: ''
}
</div>
<span part="schema-description" class='m-markdown'> ${unsafeHTML(marked(this.data?.['::description'] || ''))}</span>
${this.data
? html`
${this.generateTree(
this.data['::type'] === 'array' ? this.data['::props'] : this.data,
this.data['::type'],
this.data['::array-type'] || '',
)}`
: html`<span class='mono-font' style='color:var(--red)'> Schema not found </span>`
}
</div>
`;
}
generateTree(data, dataType = 'object', arrayType = '', key = '', description = '', schemaLevel = 0, indentLevel = 0, readOrWrite = '', isDeprecated = false) {
if (this.schemaHideReadOnly === 'true') {
if (dataType === 'array') {
if (readOrWrite === 'readonly') {
return;
}
}
if (data?.['::readwrite'] === 'readonly') {
return;
}
}
if (this.schemaHideWriteOnly === 'true') {
if (dataType === 'array') {
if (readOrWrite === 'writeonly') {
return;
}
}
if (data?.['::readwrite'] === 'writeonly') {
return;
}
}
if (!data) {
return html`<div class="null" style="display:inline;">
<span class="key-label xxx-of-key"> ${key.replace('::OPTION~', '')}</span>
${
dataType === 'array'
? html`<span class='mono-font'> [ ] </span>`
: dataType === 'object'
? html`<span class='mono-font'> { } </span>`
: html`<span class='mono-font'> schema undefined </span>`
}
</div>`;
}
if (Object.keys(data).length === 0) {
return html`<span class="key object">${key}:{ }</span>`;
}
let keyLabel = '';
let keyDescr = '';
if (key.startsWith('::ONE~OF') || key.startsWith('::ANY~OF')) {
keyLabel = key.replace('::', '').replace('~', ' ');
} else if (key.startsWith('::OPTION')) {
const parts = key.split('~');
[, keyLabel, keyDescr] = parts;
} else {
keyLabel = key;
}
const leftPadding = 12;
const minFieldColWidth = 400 - (indentLevel * leftPadding);
let openBracket = '';
let closeBracket = '';
const newSchemaLevel = data['::type']?.startsWith('xxx-of') ? schemaLevel : (schemaLevel + 1);
// const newIndentLevel = dataType === 'xxx-of-option' || data['::type'] === 'xxx-of-option' ? indentLevel : (indentLevel + 1);
const newIndentLevel = dataType === 'xxx-of-option' || data['::type'] === 'xxx-of-option' || key.startsWith('::OPTION') ? indentLevel : (indentLevel + 1);
if (data['::type'] === 'object') {
if (dataType === 'array') {
if (schemaLevel < this.schemaExpandLevel) {
openBracket = html`<span class="open-bracket array-of-object" >[{</span>`;
} else {
openBracket = html`<span class="open-bracket array-of-object">[{...}]</span>`;
}
closeBracket = '}]';
} else {
if (schemaLevel < this.schemaExpandLevel) {
openBracket = html`<span class="open-bracket object">${data['::nullable'] ? 'null┃' : ''}{</span>`;
} else {
openBracket = html`<span class="open-bracket object">${data['::nullable'] ? 'null┃' : ''}{...}</span>`;
}
closeBracket = '}';
}
} else if (data['::type'] === 'array') {
if (dataType === 'array') {
const arrType = arrayType !== 'object' ? arrayType : '';
if (schemaLevel < this.schemaExpandLevel) {
openBracket = html`<span class="open-bracket array-of-array" data-array-type="${arrType}">[[ ${arrType} </span>`;
} else {
openBracket = html`<span class="open-bracket array-of-array" data-array-type="${arrType}">[[...]]</span>`;
}
closeBracket = ']]';
} else {
if (schemaLevel < this.schemaExpandLevel) {
openBracket = html`<span class="open-bracket array">[</span>`;
} else {
openBracket = html`<span class="open-bracket array">[...]</span>`;
}
closeBracket = ']';
}
}
if (typeof data === 'object') {
return html`
<div class="tr ${schemaLevel < this.schemaExpandLevel || data['::type']?.startsWith('xxx-of') ? 'expanded' : 'collapsed'} ${data['::type'] || 'no-type-info'}${data['::nullable'] ? ' nullable' : ''}" title="${(isDeprecated || data['::deprecated']) ? 'Deprecated' : ''}">
<div class="td key ${(isDeprecated || data['::deprecated']) ? 'deprecated' : ''}" style='min-width:${minFieldColWidth}px'>
${data['::type'] === 'xxx-of-option' || data['::type'] === 'xxx-of-array' || key.startsWith('::OPTION')
? html`<span class='key-label xxx-of-key'> ${keyLabel}</span><span class="xxx-of-descr">${keyDescr}</span>`
: keyLabel === '::props' || keyLabel === '::ARRAY~OF'
? ''
: schemaLevel > 0
? html`<span class="key-label" title="${readOrWrite === 'readonly' ? 'Read-Only' : readOrWrite === 'writeonly' ? 'Write-Only' : ''}">
${(isDeprecated || data['::deprecated'])
? html`<svg viewBox="0 0 10 10" width="10" height="10" style="stroke:var(--red); margin-right:-6px"><path d="M2 2L8 8M2 8L8 2"/></svg>`
: ''
}
${keyLabel.replace(/\*$/, '')}${keyLabel.endsWith('*') ? html`<span style="color:var(--red)">*</span>` : ''}${readOrWrite === 'readonly' ? html` 🆁` : readOrWrite === 'writeonly' ? html` 🆆` : readOrWrite}:
</span>`
: ''
}
${openBracket}
</div>
<div class='td key-descr m-markdown-small'>${unsafeHTML(marked(description || ''))}</div>
</div>
<div class='inside-bracket ${data['::type'] || 'no-type-info'}' style='padding-left:${data['::type'] === 'xxx-of-option' || data['::type'] === 'xxx-of-array' ? 0 : leftPadding}px;'>
${Array.isArray(data) && data[0]
? html`${this.generateTree(data[0], 'xxx-of-option', '', '::ARRAY~OF', '', newSchemaLevel, newIndentLevel, data[0]['::readwrite'], (isDeprecated || data[0]['::deprecated']))}`
: html`
${Object.keys(data).map((dataKey) => html`
${['::title', '::description', '::type', '::props', '::deprecated', '::array-type', '::readwrite', '::dataTypeLabel', '::nullable'].includes(dataKey)
? data[dataKey]['::type'] === 'array' || data[dataKey]['::type'] === 'object'
? html`${this.generateTree(
data[dataKey]['::type'] === 'array' ? data[dataKey]['::props'] : data[dataKey],
data[dataKey]['::type'],
data[dataKey]['::array-type'] || '',
dataKey,
data[dataKey]['::description'],
newSchemaLevel,
newIndentLevel,
data[dataKey]['::readwrite'] ? data[dataKey]['::readwrite'] : '',
(isDeprecated || data[dataKey]['::deprecated']),
)}`
: ''
: html`${this.generateTree(
data[dataKey]['::type'] === 'array' ? data[dataKey]['::props'] : data[dataKey],
data[dataKey]['::type'],
data[dataKey]['::array-type'] || '',
dataKey,
data[dataKey]?.['::description'] || '',
newSchemaLevel,
newIndentLevel,
data[dataKey]['::readwrite'] ? data[dataKey]['::readwrite'] : '',
(isDeprecated || data[dataKey]['::deprecated']),
)}`
}
`)}
`
}
</div>
${data['::type'] && data['::type'].includes('xxx-of')
? ''
: html`<div class='close-bracket'> ${closeBracket} </div>`
}
`;
}
// For Primitive types and array of Primitives
// eslint-disable-next-line no-unused-vars
const [type, primitiveReadOrWrite, constraint, defaultValue, allowedValues, pattern, schemaDescription, schemaTitle, deprecated] = data.split('~|~');
if (primitiveReadOrWrite === '🆁' && this.schemaHideReadOnly === 'true') {
return;
}
if (primitiveReadOrWrite === '🆆' && this.schemaHideWriteOnly === 'true') {
return;
}
const dataTypeCss = type.replace(/┃.*/g, '').replace(/[^a-zA-Z0-9+]/g, '').substring(0, 4).toLowerCase();
const descrExpander = `${constraint || defaultValue || allowedValues || pattern ? `<span class="descr-expand-toggle ${this.schemaDescriptionExpanded === 'true' ? 'expanded-descr' : ''}">➔</span>` : ''}`;
let finalReadWriteText = '';
let finalReadWriteTip = '';
if (dataType === 'array') {
if (readOrWrite === 'readonly') {
finalReadWriteText = '🆁';
finalReadWriteTip = 'Read-Only';
} else if (readOrWrite === 'writeonly') {
finalReadWriteText = '🆆';
finalReadWriteTip = 'Write-Only';
}
} else if (primitiveReadOrWrite === '🆁') {
finalReadWriteText = '🆁';
finalReadWriteTip = 'Read-Only';
} else if (primitiveReadOrWrite === '🆆') {
finalReadWriteText = '🆆';
finalReadWriteTip = 'Write-Only';
}
return html`
<div class = "tr primitive" title="${deprecated ? 'Deprecated' : ''}">
<div class="td key ${isDeprecated || deprecated}" style='min-width:${minFieldColWidth}px'>
${isDeprecated || deprecated
? html`<svg viewBox="0 0 10 10" width="10" height="10" style="stroke:var(--red); margin-right:-6px"><path d="M2 2L8 8M2 8L8 2"/></svg>`
: ''
}
${keyLabel.endsWith('*')
? html`<span class="key-label">${keyLabel.substring(0, keyLabel.length - 1)}</span><span style='color:var(--red);'>*</span>:`
: key.startsWith('::OPTION')
? html`<span class='key-label xxx-of-key'>${keyLabel}</span><span class="xxx-of-descr">${keyDescr}</span>`
: html`<span class="key-label">${keyLabel}:</span>`
}
<span class="${dataTypeCss}" title="${finalReadWriteTip}">
${dataType === 'array' ? `[${type}]` : `${type}`}
${finalReadWriteText}
</span>
</div>
<div class='td key-descr'>
${description || schemaTitle || schemaDescription
? html`${html`<span class="m-markdown-small">
${unsafeHTML(marked(dataType === 'array'
? `${descrExpander} ${description}`
: schemaTitle
? `${descrExpander} <b>${schemaTitle}:</b> ${schemaDescription}`
: `${descrExpander} ${schemaDescription}`))}
</span>`
}`
: ''
}
${constraint ? html`<div style='display:inline-block; line-break:anywhere; margin-right:8px'><span class='bold-text'>Constraints: </span>${constraint}</div>` : ''}
${defaultValue ? html`<div style='display:inline-block; line-break:anywhere; margin-right:8px'><span class='bold-text'>Default: </span>${defaultValue}</div>` : ''}
${allowedValues ? html`<div style='display:inline-block; line-break:anywhere; margin-right:8px'><span class='bold-text'>${type === 'const' ? 'Value' : 'Allowed'}: </span>${allowedValues}</div>` : ''}
${pattern ? html`<div style='display:inline-block; line-break: anywhere; margin-right:8px'><span class='bold-text'>Pattern: </span>${pattern}</div>` : ''}
</div>
</div>
`;
}
/* eslint-enable indent */
handleAllEvents(e) {
if (e.target.classList.contains('open-bracket')) {
this.toggleObjectExpand(e);
} else if (e.target.classList.contains('schema-multiline-toggle')) {
this.schemaDescriptionExpanded = (this.schemaDescriptionExpanded === 'true' ? 'false' : 'true');
} else if (e.target.classList.contains('descr-expand-toggle')) {
const trEl = e.target.closest('.tr');
if (trEl) {
trEl.classList.toggle('expanded-descr');
trEl.style.maxHeight = trEl.scrollHeight;
}
}
}
toggleObjectExpand(e) {
const rowEl = e.target.closest('.tr');
const nullable = rowEl.classList.contains('nullable');
if (rowEl.classList.contains('expanded')) {
rowEl.classList.replace('expanded', 'collapsed');
e.target.innerHTML = e.target.classList.contains('array-of-object')
? '[{...}]'
: e.target.classList.contains('array-of-array')
? '[[...]]'
: e.target.classList.contains('array')
? '[...]'
: `${nullable ? 'null┃' : ''}{...}`;
} else {
rowEl.classList.replace('collapsed', 'expanded');
e.target.innerHTML = e.target.classList.contains('array-of-object')
? '[{'
: e.target.classList.contains('array-of-array')
? `[[ ${e.target.dataset.arrayType}`
: e.target.classList.contains('object')
? `${nullable ? 'null┃' : ''}{`
: '[';
}
}
}
customElements.define('schema-tree', SchemaTree);