pragma-views2
Version:
1,074 lines (914 loc) • 34.3 kB
JavaScript
import {
populateTemplate,
tabsheetHtml,
tabSheetButtonHtml,
tabSheetPageHtml,
groupHtml,
htmlTemplateHtml,
inputHtml,
readOnlyHtml,
textareaHtml,
cardHtmlTemplate,
buttonHtml,
dynamicHtml,
checkboxHtml,
selectHtmlForDefinedOptions,
listTemplate,
masterDetailHtml,
selectRepeatOption,
selectOption,
radioRepeatOptions,
radioGroup,
visualizationTemplate,
radioOption,
panelBarHtml,
validationsMap, splitViewHtml
} from "./template-parser-contstants.js";
import {getValueOnPath} from "./../../../../baremetal/lib/objectpath-helper.js";
export class TemplateParser {
/**
* The model that you bind to may by hidden by some object layers.
* This allows simple field definition on the template but complex binding paths.
* @param propertyPrefix
*/
constructor() {
this.parseTabSheetHandler = this.parseTabSheet.bind(this);
this.parseGroupsHandler = this.parseGroups.bind(this);
this.parseGroupHandler = this.parseGroup.bind(this);
this.parsePanelBarHandler = this.parsePanelBar.bind(this);
this.parseSplitViewHandler = this.parseSplitView.bind(this);
this.parseInputHandler = this.parseInput.bind(this);
this.parseTextAreaHandler = this.parseTextArea.bind(this);
this.parseButtonHandler = this.parseButton.bind(this);
this.parseElementsHandler = this.parseElements.bind(this);
this.parseCheckboxHandler = this.parseCheckbox.bind(this);
this.parseSelectHandler = this.parseSelect.bind(this);
this.parseCardHandler = this.parseCard.bind(this);
this.parseRadioHandler = this.parseRadio.bind(this);
this.parseTemplateHandler = this.parseTemplate.bind(this);
this.parseMasterDetailHandler = this.parseMasterDetail.bind(this);
this.parseListHandler = this.parseList.bind(this);
this.parseReadonlyHandler = this.parseReadonly.bind(this);
this.parseVisualizationHandler = this.parseVisualization.bind(this);
this.parseHtmlTemplateHandler = this.parseHtmlTemplateHandler.bind(this);
this.parseMap = new Map();
this.parseMap.set("tabsheet", this.parseTabSheetHandler);
this.parseMap.set("groups", this.parseGroupsHandler);
this.parseMap.set("group", this.parseGroupHandler);
this.parseMap.set("panel-bar", this.parsePanelBarHandler);
this.parseMap.set("split-view", this.parseSplitViewHandler);
this.parseMap.set("input", this.parseInputHandler);
this.parseMap.set("memo", this.parseTextAreaHandler);
this.parseMap.set("button", this.parseButtonHandler);
this.parseMap.set("elements", this.parseElementsHandler);
this.parseMap.set("checkbox", this.parseCheckboxHandler);
this.parseMap.set("select", this.parseSelectHandler);
this.parseMap.set("card", this.parseCardHandler);
this.parseMap.set("radio", this.parseRadioHandler);
this.parseMap.set("template", this.parseTemplateHandler);
this.parseMap.set("master-detail", this.parseMasterDetailHandler);
this.parseMap.set("list", this.parseListHandler);
this.parseMap.set("readonly", this.parseReadonlyHandler);
this.parseMap.set("visualization", this.parseVisualizationHandler);
this.parseMap.set("html-template", this.parseHtmlTemplateHandler);
}
/**
* @destructor
*/
dispose() {
this.variables = null;
this.datasets = null;
this.lookups = null;
this.datasources = null;
this.parseMap.clear();
this.parseMap = null;
this.parseTabSheetHandler = null;
this.parseGroupsHandler = null;
this.parseGroupHandler = null;
this.parseSplitViewHandler = null;
this.parsePanelBarHandler = null;
this.parseInputHandler = null;
this.parseTextAreaHandler = null;
this.parseButtonHandler = null;
this.parseElementsHandler = null;
this.parseCheckboxHandler = null;
this.parseSelectHandler = null;
this.parseCardHandler = null;
this.parseRadioHandler = null;
this.parseTemplateHandler = null;
this.parseHtmlTemplateHandler = null;
this.lookups = null;
this.datasets = null;
this.datasources = null;
this.templates = null;
this.perspectives = null;
this.previews = null;
}
/**
* Process the json template
* @param json: template structure to process
* @returns {string} html result
*/
parse(json) {
return new Promise(resolve => {
this.initializeResources(json);
const result = this.parseObject(json.body);
resolve(result);
});
}
initializeResources(json) {
this.lookups = json.lookups;
this.datasets = json.datasets;
this.datasources = json.datasources;
this.templates = json.templates;
this.perspectives = json.perspectives;
this.previews = json.previews;
this.variables = json.variables;
this.datasetMap = new Map();
this.indexDatasets("model", 0);
}
indexDatasets(name, id) {
if (this.datasets == undefined) {
return;
}
const dataset = this.datasets.find(item => item.id == id);
if (dataset == undefined) {
return;
}
this.datasetMap.set(name, dataset);
for (let field of dataset.fields) {
if (field.dataset != undefined) {
this.indexDatasets(`${name}.${field.name}`, field.dataset);
}
}
}
replaceVariableMarker(text) {
if (text.indexOf("@") == -1) {
return text;
}
return text.split("@").join("schema.variables.");
}
getVariableValue(path) {
if (path[0] != "@") return path;
if (this.variables == undefined) return undefined;
path = path.split("@").join("");
return getValueOnPath(this.variables, path);
}
/**
* This function allows you to bind to a schema variable.
* This assumes a pragma-form consumption path of schema.variables.variablename
* @param varContent
* @returns {*}
*/
varToBiding(varContent) {
if (varContent[0] == "@") {
return `schema.variables.${varContent.slice(1)}`;
}
return varContent;
}
/**
* This function allows you to bind to a schema variable as a content binding using ${schema.variables.variablename}
* @param varContent
* @returns {*}
*/
varToContentBinding(varContent) {
if (varContent == undefined) {
return varContent;
}
if (varContent[0] == "@") {
return '${' + `schema.variables.${varContent.slice(1)}}`;
}
return varContent;
}
/**
* search fieldmap for a comparison key as defined by field
* @param field
* @returns {*}
*/
getField(field, context){
if (context != undefined) {
return this.varToBiding(`${context}.${field}`);
}
return this.varToBiding(field);
}
/**
* Get the datasource witht he following id
* @param id
* @returns {null}
*/
getDatasource(id) {
if (isNaN(id)) {
return id;
}
if (this.datasources == undefined || id == undefined) {
return null;
}
const ds = this.datasources.find(ds => ds.id.toString() == id.toString());
if (ds.field != undefined) {
return ds.field;
}
return ds;
}
/**
* Get a particular template by id
* @param id
* @returns {*}
*/
getTemplate(id) {
if (this.templates == undefined) {
return null;
}
return this.templates.find(template => template.id.toString() == id.toString());
}
/**
* Parse unknown object for particulars and navigate from here to more appropriate generators
* @param obj: object to parse
*/
parseObject(obj) {
if (!obj) {
return false;
}
const properties = Object.keys(obj);
const result = [];
for(let property of properties) {
const propertyObect = obj[property];
if (this.isKnownType(property)) {
result.push(this.parseKnown(property, propertyObect));
}
else {
result.push(this.parseObject(propertyObect));
}
}
return result.join("");
}
/**
* Evaluate if this property is a known key for specific parsing
* @param property
* @returns {boolean}
*/
isKnownType(property) {
return this.parseMap.has(property);
}
/**
* Get the parser for a particular property and process the given object with that parser
* @param property: key for the parser to extract
* @param obj: object that needs to be parsed
* @returns {string} html result
*/
parseKnown(property, obj) {
return this.parseMap.get(property)(obj);
}
/**
* Parse the object as a tabsheet and generage pragma-pager custom element html markup for it.
* @param tabsheet: Tabsheet object, should be array of tabs
* @return {string}
*/
parseTabSheet(tabsheet) {
const classes = this.processClasses(tabsheet);
const attributes = this.processAttributes(tabsheet);
const tabSheetButtonsHTML = this.parseTabSheetButtons(tabsheet.elements);
const tabSheetPagesHTML = this.parseTabSheetPages(tabsheet.elements);
const result = populateTemplate(tabsheetHtml, {
"__classes__": classes,
"__attributes__": attributes,
"__tabSheetButtons__": tabSheetButtonsHTML,
"__tabSheetPages__": tabSheetPagesHTML
});
return result;
}
/**
* Parse array of objects as pager-button custom elements.
* @param tabSheetButtons: array of objects to parse
* @return {string}
*/
parseTabSheetButtons(tabSheetButtons) {
const result = [];
for(const tabSheetButton of tabSheetButtons) {
result.push(populateTemplate(tabSheetButtonHtml, {
"__id__": tabSheetButton.id,
"__title__": this.getVariableValue(tabSheetButton.title)
}))
}
return result.join("");
}
/**
* Parse array of objects as block elements (div) with class tabsheet-page.
* The object should have the following properties
* 1. id: unique identifier for the tab
* 2. title: title to display at the top of the group
* 3. groups: array of groups
* @param tabSheetPages: array of objects to parse
* @return {string}
*/
parseTabSheetPages(tabSheetPages) {
const result = [];
for(let tabSheetPage of tabSheetPages) {
const template = this.getTemplate(tabSheetPage.template);
const content = this.parseElements(template.elements);
result.push(populateTemplate(tabSheetPageHtml, {
"__id__": tabSheetPage.id,
"__title__": this.getVariableValue(tabSheetPage.title),
"__content__": this.replaceVariableMarker(content)
}))
}
return result.join("");
}
/**
* Remove all relative path markup from string
* @param path
* @returns {string}
*/
cleanRelative(path) {
return path.split("../").join("");
}
/**
* Parse a object as a group.
* The object is expected to be an array of groups.
* Each group must have the following fields:
* 1. title: string to display as title of the group
* 2. items: array fields that must be rendered. see parseElements
* @param obj: object to parse
*/
parseGroups(groups) {
const result = [];
for (let group of groups) {
result.push(this.parseGroup(group));
}
return result.join("");
}
/**
* Parse a single group and it's content
* @param element
* @returns {*}
*/
parseGroup(element) {
const classes = this.processClasses(element);
const attributes = this.processAttributes(element);
const fieldsHtml = this.parseElements(element.elements);
return populateTemplate(groupHtml, {
"__title__": this.getVariableValue(element.title),
"__content__": fieldsHtml,
"__attributes__": attributes,
"__classes__": classes
});
}
/**
* Parse a panel bar and it's content
* @param element
* @returns {*}
*/
parsePanelBar(element) {
const classes = this.processClasses(element);
const attributes = this.processAttributes(element);
const fieldsHtml = this.parseElements(element.elements);
const actionsHtml = this.parseElements(element.actions);
return populateTemplate(panelBarHtml, {
"__title__": this.getVariableValue(element.title),
"__content__": fieldsHtml,
"__actions__": actionsHtml,
"__attributes__": attributes,
"__classes__": classes
});
}
/**
* Parse a panel bar and it's content
* @param element
* @returns {*}
*/
parseSplitView(element) {
const classes = this.processClasses(element);
const attributes = this.processAttributes(element);
const leftHtml = this.parseElements(element.left);
const rightHtml = this.parseElements(element.right);
return populateTemplate(splitViewHtml, {
"__left__": leftHtml,
"__right__": rightHtml,
"__attributes__": attributes,
"__classes__": classes
});
}
/**
* Parse a object as a input type
* The object must contain the following fields:
* 1. element: used to determine how to process the input type
* @param obj
*/
parseElements(elements, context) {
if (!elements) {
return "";
}
const result = [];
for (let element of elements) {
result.push(this.parseElement(element, context));
}
return result.join("");
}
/**
* Parse checkbox
* @param element
* @returns {*}
*/
parseCheckbox(element, context) {
const title = this.getVariableValue(element.title);
const field = this.getField(element.field, context);
const description = element.description || "";
const classes = this.processClasses(element);
const attributes = this.processAttributes(element);
return populateTemplate(checkboxHtml, {
"__field__": field,
"__title__": title,
"__description__": description,
"__classes__": classes,
"__attributes__": attributes
});
}
/**
* Parse a individual element and generate the direct it to the appropriate generateor
* The object being parsed must have the following fields:
* 1. element
* @param element: object to parse
*/
parseElement(element, context) {
const elementType = element.element;
if (this.isKnownType(elementType)) {
return this.parseMap.get(elementType)(element, context);
}
else {
return this.parseUnknown(element, context);
}
}
/**
* Parse element on a dynamic level interpreting custom elements
* @param element
*/
parseUnknown(element) {
const classes = this.processClasses(element);
const attributes = this.processAttributes(element);
const content = this.varToContentBinding(element.content) || this.parseElements(element.elements);
return populateTemplate(dynamicHtml, {
"__tagname__": element.element,
"__classes__": classes,
"__attributes__": attributes,
"__content__": this.replaceVariableMarker(content),
});
}
/**
* Parse attributes defined in template to be part of the html
* @param obj
* @return {string}
*/
processAttributes(obj) {
const attributes = [];
if (obj.attributes) {
const attrKeys = Object.keys(obj.attributes);
for(let attrKey of attrKeys) {
const attrValue = obj.attributes[attrKey];
const value = attrKey.indexOf(".bind") != -1 ? this.varToBiding(attrValue) : this.varToContentBinding(obj.attributes[attrKey]);
attributes.push(`${attrKey}="${value}"`);
}
}
return attributes.join(" ");
}
/**
* Process style classes
* @param obj
* @return {*}
*/
processClasses(obj) {
if (obj.styles) {
if (Array.isArray(obj.styles)) {
return `class="${obj.styles.join(" ")}"`;
}
return `class="${obj.styles}"`;
}
return "";
}
/**
* Parse the object as a input composite
* Properties that should be supplied are:
* 1. title
* 2. field
* 3. type
*
* Additional properties you can define are:
* 1. attributes: object literal
* 2. classes: array of string
* 3. descriptor
*
* Items that are lookup items must have the attribute "data-lookup" defined
* @param input
*/
parseInput(input, context) {
let field = this.getField(input.field, context);
const title = this.getVariableValue(input.title);
const pathParts = field.split(".");
const modelKey = pathParts[0];
const fieldName = pathParts[1];
let validationBehaviours = "";
const ds = this.datasetMap.get(modelKey);
if (ds != undefined) {
const fieldDef = ds.fields.find(item => item.name == fieldName);
validationBehaviours = this.getValidationBehaviours(fieldDef.validations);
if (validationBehaviours.length > 0) {
validationBehaviours = `behaviours="${validationBehaviours}"`;
}
}
const required = input.required || false;
const classes = this.processClasses(input);
const attributes = this.processAttributes(input);
const descriptor = this.getDescriptor(input);
const lookup = this.getLookup(input, context);
const peek = this.getPeek(input, context);
let tableBinding = "";
if (lookup.length > 0) {
const path = this.getTablePath(input, context);
tableBinding = `model.bind="${path}"`
}
let result = populateTemplate(inputHtml, {
"__peek__": peek || "",
"__model__": tableBinding,
"__lookup__": lookup,
"__field__": field,
"__title__": title,
"__description__": descriptor,
"__classes__": classes,
"__attributes__": attributes,
"__required__": required,
"__behaviours__": validationBehaviours
});
return result;
}
getValidationBehaviours(validation) {
if (validation == undefined) {
return "";
}
const result = [];
const keys = Object.keys(validation);
keys.forEach(key => {
if (key != "null") {
let str = validationsMap.get(key);
const value = validation[key].value;
if (value != undefined) {
str = str.split("__value__").join(value)
}
result.push(str);
}
})
return result.join(",");
}
getLookup(field, context) {
const path = context == undefined ? field.field : `${context}.${field.field}`;
const items = path.split(".");
const fld = items.splice(items.length - 1, 1);
const modelKey = items.join(".");
if (modelKey.length == "") {
return "";
}
const model = this.datasetMap.get(modelKey);
if (model == undefined) {
return "";
}
const lookupField = model.fields.find(item => item.name == fld);
if (lookupField == undefined) {
return "";
}
if (lookupField.lookup == undefined) {
return "";
}
return `lookup="${lookupField.name}"`;
}
getPeek(field, context) {
const path = context == undefined ? field.field : `${context}.${field.field}`;
const items = path.split(".");
const fld = items.splice(items.length - 1, 1);
const modelKey = items.join(".");
if (modelKey.length == "") {
return "";
}
const model = this.datasetMap.get(modelKey);
if (model == undefined) {
return "";
}
const peekField = model.fields.find(item => item.name == fld);
if (peekField == undefined) {
return "";
}
if (peekField.preview == undefined) {
return "";
}
const preview = this.previews.find(item => item.id == peekField.preview);
if (preview == undefined) {
return "";
}
if (preview["dataset-field"] == undefined) {
return "";
}
return `peek="${peekField.name}"`;
}
getTablePath(field, context) {
const path = context == undefined ? field.field : `${context}.${field.field}`;
const items = path.split(".");
items.splice(items.length - 1, 1);
const modelKey = items.join(".");
return modelKey;
}
parseReadonly(element, context) {
const title = this.getVariableValue(element.title);
const classes = this.processClasses(element);
const attributes = this.processAttributes(element);
const field = this.getField(element.field, context);
let result = populateTemplate(readOnlyHtml, {
"__field__": `${field}_readonly`,
"__content__": "${" + field + "}",
"__title__": title,
"__classes__": classes,
"__attributes__": attributes
});
return result;
}
/**
* Parse a given schema element and determine if the descriptor should use binding or string constant values
* @param element: element to process
* @returns : string value for descriptor
*/
getDescriptor(element) {
const description = element.description || "";
let descriptor = element.descriptor || "";
// Nothing set return descriptor empty context
if (description.length == descriptor.length == 0) {
return "descriptor=''";
}
// description set so return binding expression
if (description.length > 0) {
return `descriptor.bind="${description}"`;
}
// descriptor used so send back descriptor text with out binding
return `descriptor="${descriptor}"`;
}
/**
* Parse the object as a textarea composite
* Properties that should be supplied are:
* 1. title
* 2. field
*
* Additional properties you can define are:
* 1. attributes: object literal
* 2. classes: array of string
* 3. descriptor
*
* @param textaria
*/
parseTextArea(memo, context) {
const title = this.getVariableValue(memo.title);
const field = this.getField(memo.field, context);
const description = memo.descriptor || "";
const required = memo.required || false;
const classes = this.processClasses(memo);
const attributes = this.processAttributes(memo);
let descriptor = memo.descriptor || "";
if (description.length > 0) {
descriptor = `descriptor.bind="${description}"`
}
else {
descriptor = `descriptor="${descriptor}"`
}
return populateTemplate(textareaHtml, {
"__field__": field,
"__title__": title,
"__description__": descriptor,
"__classes__": classes,
"__attributes__": attributes,
"__required__": required
});
}
/**
* Parse object as button
* Properties that should be provided:
* 1. title
* 2. action
* @param button
* @return {*}
*/
parseButton(button) {
const title = this.getVariableValue(button.title);
const attributes = this.processAttributes(button);
const classes = this.processClasses(button);
let action;
if (button.process != undefined) {
action = `performProcess(${button.process})`;
}
else {
action = `performAction(${button.action}, ${button.closeDialog || false})`;
}
return populateTemplate(buttonHtml, {
"__title__": title,
"__action__": action,
"__classes__": classes,
"__attributes__": attributes
});
}
/**
* Parse select options and fill in as per datasource definitions
* @param select
*/
parseSelect(select) {
const title = this.varToContentBinding(select.title);
const datasource = select.datasource;
const field = this.varToBiding(select.field);
const classes = this.processClasses(select);
const attributes = this.processAttributes(select);
const required = select.required || false;
const descriptor = this.getDescriptor(select);
let content = "";
const ds = this.getDatasource(datasource);
if (ds == null || ds == undefined) {
console.error(`select "${title}"'s datasource does not exist in schema`);
return "";
}
if (typeof ds == "string" || ds.field != undefined) {
const repeatField = ds.field || ds;
content = populateTemplate(selectRepeatOption, {
"__datasource__": repeatField,
"__content__": "${title}"
})
}
else {
if (!Array.isArray(ds.resource)) {
console.error(`resouce was expected to be an array for ${title}`);
return "";
}
for (let resource of ds.resource) {
const id = resource.id;
const title = this.varToContentBinding(resource.title);
content = content + populateTemplate(selectOption, {
"__option-id__": id,
"__content__": title
})
}
}
let result = populateTemplate(selectHtmlForDefinedOptions, {
"__field__": field,
"__title__": this.getVariableValue(title),
"__classes__": classes,
"__attributes__": attributes,
"__required__": required == true ? required : "",
"__description__": descriptor,
"__content__": content,
"__datasource__": datasource
});
return result;
}
/**
* Parse card schema
* @param card
* @returns {*}
*/
parseCard(card) {
const content = this.parseElements(card.elements);
return populateTemplate(cardHtmlTemplate, {
"__content__": content
})
}
/**
* Parse radio schema and datasource options
* @param radio
* @returns {string}
*/
parseRadio(radio) {
const datasource = radio.datasource;
const field = this.varToBiding(radio.field);
const classes = this.processClasses(radio);
const attributes = this.processAttributes(radio);
let content = "";
const ds = this.getDatasource(datasource);
if (ds == null || ds == undefined) {
console.error(`radio's datasource does not exist in schema`);
return "";
}
if (ds.field != undefined) {
content = populateTemplate(radioRepeatOptions, {
"__datasource__": ds.field,
"__content__": "${title}",
"__groupname__": ds.id,
"__field__": field
})
}
else {
if (!Array.isArray(ds.resource)) {
console.error(`radio's resouce was expected to be an array`);
return "";
}
for (let resource of ds.resource) {
const id = resource.id;
const title = this.varToContentBinding(resource.title);
content = content + populateTemplate(radioOption, {
"__option-id__": id,
"__content__": this.replaceVariableMarker(title),
"__groupname__": ds.id,
"__field__": field
})
}
}
const group = populateTemplate(radioGroup, {
"__content__": content,
"__classes__": classes,
"__attributes__": attributes
});
return group;
}
/**
* Parse template
* @param tmpl
* @returns {*}
*/
parseTemplate(tmpl) {
const id = tmpl.template;
const condition = this.conditionToVarBinding(tmpl.condition);
const template = this.getTemplate(id);
const context = tmpl.context;
if (template == null) {
console.error(`template with id ${id} was not found`);
return ""
}
const content = this.parseElements(template.elements, context);
let startTag = `<div if.bind="__condition__" data-template="${id}">`;
const endTag = "</div>";
if (condition.length == 0) {
startTag = startTag.replace('if.bind="__condition__"', "");
}
else {
startTag = startTag.replace('__condition__', condition);
}
return `${startTag}${content}${endTag}`;
}
conditionToVarBinding(condition) {
if (condition == undefined) {
return "";
}
const parts = condition.split(" ");
parts[0] = this.varToBiding(parts[0]);
return parts.join(" ");
}
parseMasterDetail(md) {
const master = md.master;
const detail = md.detail;
const masterContent = this.parseElements(master);
const detailContent = this.parseElements(detail);
const classes = this.processClasses(md);
const attributes = this.processAttributes(md);
const result = populateTemplate(masterDetailHtml, {
"__classes__": classes,
"__attributes__": attributes,
"__master__": masterContent,
"__detail__": detailContent
});
return result;
}
parseList(list) {
const datasourceId = list.datasource;
const datasource = this.getDatasource(datasourceId);
const templateId = list.template;
const template = this.getTemplate(templateId);
const templateContent = this.parseElements(template.elements);
const classes = this.processClasses(list);
const attributes = this.processAttributes(list);
const result = populateTemplate(listTemplate, {
"__datasource__": datasource,
"__template__": templateContent,
"__idField__": list.idField || "id",
"__selection__": list.selection || "single",
"__classes__": classes,
"__attributes__": attributes
});
return result;
}
parseVisualization(element) {
const datasourceId = element.datasource;
const datasource = this.getDatasource(datasourceId);
const perspective = element.perspective;
const view = element.view;
const template = this.perspectives.find(item => item.id == perspective).views.find(item => item.id == view).template;
const classes = this.processClasses(element);
const attributes = this.processAttributes(element);
const templateBinding = template == undefined ? "" : `template.bind="template(${template})"`;
const result = populateTemplate(visualizationTemplate, {
"__classes__": classes,
"__attributes__": attributes,
"__datasource__": datasource,
"__perspective__": perspective,
"__view__": view,
"__template__": templateBinding
});
return result;
}
parseHtmlTemplateHandler(element) {
const classes = this.processClasses(element);
const attributes = this.processAttributes(element);
const fieldsHtml = this.parseElements(element.elements);
return populateTemplate(htmlTemplateHtml, {
"__content__": fieldsHtml,
"__attributes__": attributes,
"__classes__": classes
});
}
}