craftToSaltcorn = (nodes, startFrom = "ROOT", options) => {
  //console.log(JSON.stringify(nodes, null, 2));
  var columns = [];
  /**
   * @param {object} node
   * @returns {void|object}
   */
  const removeEmpty = ({ above }) => {
    const valids = above.filter(Boolean);
    if (valids.length === 1) return valids[0];
    else return { above: valids };
  };

  const get_nodes = (node) => {
    if (!node.nodes || node.nodes.length == 0) return;
    else if (node.nodes.length == 1) return go(nodes[node.nodes[0]]);
    else return removeEmpty({ above: node.nodes.map((nm) => go(nodes[nm])) });
  };

  /**
   * @param {object} node
   * @returns {object}
   */
  const go = (node) => {
    if (!node) return;
    let customProps = {};
    if (Object.keys(node?.custom || {}).length)
      customProps = { _custom: { ...node?.custom } };
    const matchElement = allElements.find(
      (e) =>
        e.craft.related &&
        node.displayName === e.craft.displayName &&
        e.craft.related.fields &&
        e.craft.related.segment_type
    );
    if (matchElement) {
      const related = matchElement.craft.related;
      const s = { type: related.segment_type, ...customProps };
      if (related.hasContents) s.contents = get_nodes(node);
      related.fields.forEach((f) => {
        if (f.type === "Nodes" && f.nodeID) {
          //console.log("nodetype", node);
          s[f.segment_name || f.name || f] = go(
            nodes[node.linkedNodes[f.nodeID]]
          );
        } else s[f.segment_name || f.name || f] = node.props[f.name || f];
        if(f.onSave) f.onSave(s, node.props)
      });
      if (related.fields.some((f) => f.canBeFormula))
        s.isFormula = node.props.isFormula;
      if (related.segment_vars) Object.assign(s, related.segment_vars);
      if (related.column_type) {
        const c = { type: related.column_type };
        related.fields.forEach((f) => {
          c[f.column_name || f.name || f] = node.props[f.name || f];
        });
        if (s.isFormula) c.isFormula = s.isFormula;
        columns.push(c);
      }
      return s;
    }
    if (node.displayName === ListColumns.craft.displayName) {
      return {
        besides: node.nodes.map((nm) => go(nodes[nm])),
        list_columns: true,
        ...customProps,
      };
    }
    if (node.displayName === ListColumn.craft.displayName) {
      const contents = go(nodes[node.linkedNodes.listcol]);
      const addFields = options.additionalColumnFields;
      const lc = {
        contents,
        col_width: node.props.col_width,
        col_width_units: node.props.col_width_units,
        alignment: node.props.alignment,
        header_label: node.props.header_label,
        showif: node.props.showif,
        ...customProps,
      };
      (addFields || []).forEach((f) => {
        lc[f.name] = node.props[f.name];
      });
      return lc;
    }
    if (node.isCanvas) {
      if (node.displayName === Container.craft.displayName)
        return {
          contents: get_nodes(node),
          type: "container",
          customCSS: node.props.customCSS,
          customClass: node.props.customClass,
          customId: node.props.customId,
          animateName: node.props.animateName,
          animateDelay: node.props.animateDelay,
          animateDuration: node.props.animateDuration,
          animateInitialHide: node.props.animateInitialHide,
          minHeight: node.props.minHeight,
          height: node.props.height,
          width: node.props.width,
          url: node.props.url,
          hoverColor: node.props.hoverColor,
          minHeightUnit: node.props.minHeightUnit,
          heightUnit: node.props.heightUnit,
          widthUnit: node.props.widthUnit,
          vAlign: node.props.vAlign,
          hAlign: node.props.hAlign,
          htmlElement: node.props.htmlElement,
          margin: node.props.margin,
          padding: node.props.padding,
          overflow: node.props.overflow,
          display: node.props.display,
          fullPageWidth: node.props.fullPageWidth || false,
          bgFileId: node.props.bgFileId,
          bgField: node.props.bgField,
          bgType: node.props.bgType,
          imageSize: node.props.imageSize,
          imgResponsiveWidths: node.props.imgResponsiveWidths,
          bgColor: node.props.bgColor,
          setTextColor: node.props.setTextColor,
          textColor: node.props.textColor,
          isFormula: node.props.isFormula,
          showIfFormula: node.props.showIfFormula,
          showForRole: node.props.showForRole,
          minScreenWidth: node.props.minScreenWidth,
          maxScreenWidth: node.props.maxScreenWidth,
          show_for_owner: node.props.show_for_owner,
          gradStartColor: node.props.gradStartColor,
          gradEndColor: node.props.gradEndColor,
          gradDirection: node.props.gradDirection,
          click_action: node.props.click_action,
          rotate: node.props.rotate,
          style: node.props.style,
          transform: node.props.transform,
          ...customProps,
        };
      else return get_nodes(node);
    }

    if (node.displayName === Text.craft.displayName) {
      return {
        type: "blank",
        contents: node.props.text,
        block: node.props.block,
        inline: node.props.inline,
        textStyle: node.props.textStyle,
        isFormula: node.props.isFormula,
        labelFor: node.props.labelFor,
        customClass: node.props.customClass,
        style: node.props.style,
        icon: node.props.icon,
        font: node.props.font,
        ...customProps,
      };
    }

    if (node.displayName === Table.craft.displayName) {
      const rows = node.props.rows;
      const columns = node.props.columns;

      const contents = ntimes(rows, (ri) =>
        ntimes(columns, (ci) => go(nodes[node.linkedNodes[`cell_${ri}_${ci}`]]))
      );
      return {
        type: "table",
        rows,
        columns,
        contents,
        bs_style: node.props.bs_style,
        customClass: node.props.customClass,
        bs_small: node.props.bs_small,
        bs_striped: node.props.bs_striped,
        bs_bordered: node.props.bs_bordered,
        bs_borderless: node.props.bs_borderless,
        bs_wauto: node.props.bs_wauto,
        ...customProps,
      };
    }

    if (node.displayName === Columns.craft.displayName) {
      const widths = [...node.props.widths, 12 - sum(node.props.widths)];
      return {
        besides: widths.map((w, ix) => go(nodes[node.linkedNodes["Col" + ix]])),
        breakpoints: node.props.breakpoints,
        customClass: node.props.customClass,
        gx: +node.props.gx,
        gy: +node.props.gy,
        aligns: node.props.aligns,
        vAligns: node.props.vAligns,
        colClasses: node.props.colClasses,
        colStyles: node.props.colStyles,
        style: node.props.style,
        widths,
        ...customProps,
      };
    }
    if (node.displayName === Tabs.craft.displayName) {
      let contents;
      if (node.props.tabsStyle === "Value switch") {
        contents = node.props.titles.map(({ value }, ix) => {
          const useIx = typeof value === "undefined" ? ix : value;
          return go(nodes[node.linkedNodes["Tab" + useIx]]);
        });
      } else
        contents = ntimes(node.props.ntabs, (ix) =>
          go(nodes[node.linkedNodes["Tab" + ix]])
        );
      return {
        type: "tabs",
        contents,
        titles: node.props.titles,
        showif: node.props.showif,
        tabsStyle: node.props.tabsStyle,
        field: node.props.field,
        independent: node.props.independent,
        startClosed: node.props.startClosed,
        acc_init_opens: node.props.acc_init_opens,
        deeplink: node.props.deeplink,
        disable_inactive: node.props.disable_inactive,
        serverRendered: node.props.serverRendered,
        tabId: node.props.tabId,
        ntabs: node.props.ntabs,
        setting_tab_n: node.props.setting_tab_n,
        ...customProps,
      };
    }

    if (node.displayName === View.craft.displayName) {
      return {
        type: "view",
        view: node.props.view,
        relation: node.props.relation,
        order_field: node.props.order_field,
        name:
          node.props.name === "not_assigned" ? rand_ident() : node.props.name,
        state: node.props.state,
        configuration: node.props.configuration,
        extra_state_fml: node.props.extra_state_fml,
        ...customProps,
      };
    }
    if (node.displayName === Action.craft.displayName) {
      const newid = rand_ident();
      columns.push({
        type: "Action",
        action_name: node.props.name,
        ...(node.props.name !== "Clear" && node.props.action_row_variable
          ? {
              action_row_variable: node.props.action_row_variable,
              action_row_limit: node.props.action_row_limit,
            }
          : {}),
        action_label: node.props.action_label,
        action_style: node.props.action_style,
        action_size: node.props.action_size,
        action_icon: node.props.action_icon,
        action_title: node.props.action_title,
        action_class: node.props.action_class,
        action_bgcol: node.props.action_bgcol,
        action_bordercol: node.props.action_bordercol,
        action_textcol: node.props.action_textcol,
        minRole: node.props.minRole,
        confirm: node.props.confirm,
        spinner: node.props.spinner,
        is_submit_action: node.props.is_submit_action,
        nsteps: node.props.nsteps,
        step_only_ifs: node.props.step_only_ifs,
        step_action_names: node.props.step_action_names,
        configuration: node.props.configuration,
        isFormula: node.props.isFormula,
        rndid: node.props.rndid === "not_assigned" ? newid : node.props.rndid,
      });
      return {
        type: "action",
        block: node.props.block,
        configuration: node.props.configuration,
        confirm: node.props.confirm,
        is_submit_action: node.props.is_submit_action,
        action_name: node.props.name,
        ...(node.props.name !== "Clear" && node.props.action_row_variable
          ? {
              action_row_variable: node.props.action_row_variable,
              action_row_limit: node.props.action_row_limit,
            }
          : {}),
        action_label: node.props.action_label,
        action_style: node.props.action_style,
        action_size: node.props.action_size,
        action_icon: node.props.action_icon,
        action_title: node.props.action_title,
        action_class: node.props.action_class,
        action_bgcol: node.props.action_bgcol,
        spinner: node.props.spinner,
        action_bordercol: node.props.action_bordercol,
        action_textcol: node.props.action_textcol,
        nsteps: node.props.nsteps,
        step_only_ifs: node.props.step_only_ifs,
        step_action_names: node.props.step_action_names,
        minRole: node.props.minRole,
        isFormula: node.props.isFormula,
        rndid: node.props.rndid === "not_assigned" ? newid : node.props.rndid,
        ...customProps,
      };
    }
  };
  const layout = go(nodes[startFrom]) || { type: "blank", contents: "" };
  /*console.log("nodes", JSON.stringify(nodes));
    console.log("cols", JSON.stringify(columns));
  console.log("layout", JSON.stringify(layout));*/
  return { columns, layout };
}
@param {object[]} nodes
@param {string} [startFrom = "ROOT" ]
@returns {object}
@category saltcorn-builder
@subcategory components
@namespace