private void renderNotebookv2WithDialog(final DocUpdateSentinel sourceDoc) {
    // default format
    String format = sourceDoc.getProperty(NOTEBOOK_FORMAT);
    if (StringUtil.isNullOrEmpty(format)) {
      format = prefs_.compileNotebookv2Options().getValue().getFormat();
      if (StringUtil.isNullOrEmpty(format)) format = CompileNotebookv2Options.FORMAT_DEFAULT;
    }

    CompileNotebookv2OptionsDialog dialog =
        new CompileNotebookv2OptionsDialog(
            format,
            new OperationWithInput<CompileNotebookv2Options>() {
              @Override
              public void execute(CompileNotebookv2Options input) {
                renderNotebookv2(sourceDoc, input.getFormat());

                // save options for this document
                HashMap<String, String> changedProperties = new HashMap<String, String>();
                changedProperties.put(NOTEBOOK_FORMAT, input.getFormat());
                sourceDoc.modifyProperties(changedProperties, null);

                // save global prefs
                CompileNotebookv2Prefs prefs = CompileNotebookv2Prefs.create(input.getFormat());
                if (!CompileNotebookv2Prefs.areEqual(
                    prefs, prefs_.compileNotebookv2Options().getValue())) {
                  prefs_.compileNotebookv2Options().setGlobalValue(prefs);
                  prefs_.writeUIPrefs();
                }
              }
            });
    dialog.showModal();
  }
  private boolean recomputeVisibility() {
    // if all publishing is disabled, hide ourselves
    if (!session_.getSessionInfo().getAllowPublish()
        || !pUiPrefs_.get().showPublishUi().getGlobalValue()) return false;

    // if both internal and external publishing is disabled, hide ourselves
    if (!session_.getSessionInfo().getAllowExternalPublish()
        && !pUiPrefs_.get().enableRStudioConnect().getGlobalValue()) return false;

    // if we're bound to a command's visibility/enabled state, check that
    if (boundCommand_ != null && (!boundCommand_.isVisible() || !boundCommand_.isEnabled()))
      return false;

    // if we have no content type, hide ourselves
    if (contentType_ == RSConnect.CONTENT_TYPE_NONE) return false;

    // if we do have a content type, ensure that we have actual content
    // bound to it
    if ((contentType_ == RSConnect.CONTENT_TYPE_HTML
            || contentType_ == RSConnect.CONTENT_TYPE_PLOT
            || contentType_ == RSConnect.CONTENT_TYPE_PRES)
        && publishHtmlSource_ == null) return false;

    if ((contentType_ == RSConnect.CONTENT_TYPE_APP
            || contentType_ == RSConnect.CONTENT_TYPE_APP_SINGLE)
        && StringUtil.isNullOrEmpty(contentPath_)) return false;

    if (manuallyHidden_) return false;

    // looks like we should be visible
    return true;
  }
  public void setRmd(String rmd, boolean isStatic) {
    docPreview_ = new RenderedDocPreview(rmd, "", isStatic);
    setContentPath(rmd, "");

    SessionInfo sessionInfo = session_.getSessionInfo();
    String buildType = sessionInfo.getBuildToolsType();

    boolean setType = false;
    if (buildType.equals(SessionInfo.BUILD_TOOLS_WEBSITE)) {
      // if this is an Rmd with a content path
      if (contentType_ == RSConnect.CONTENT_TYPE_DOCUMENT
          && !StringUtil.isNullOrEmpty(contentPath_)) {
        // ...and if the content path is within the website dir,
        String websiteDir = sessionInfo.getBuildTargetDir();
        if (contentPath_.startsWith(websiteDir)) {
          setType = true;
          setContentType(RSConnect.CONTENT_TYPE_WEBSITE);
        }
      }
    }

    // if we haven't set the type yet, apply it
    if (!setType) setContentType(RSConnect.CONTENT_TYPE_DOCUMENT);

    applyVisiblity();
  }
Esempio n. 4
0
  public void onSendToConsole(final SendToConsoleEvent event) {
    final InputEditorDisplay display = view_.getInputEditorDisplay();

    // get anything already at the console
    final String previousInput = StringUtil.notNull(display.getText());

    // define code block we execute at finish
    Command finishSendToConsole =
        new Command() {
          @Override
          public void execute() {
            if (event.shouldExecute()) {
              processCommandEntry();
              if (previousInput.length() > 0) display.setText(previousInput);
            }

            if (!event.shouldExecute() || event.shouldFocus()) {
              display.setFocus(true);
              display.collapseSelection(false);
            }
          }
        };

    // do standrd finish if we aren't animating
    if (!event.shouldAnimate()) {
      display.clear();
      display.setText(event.getCode());
      finishSendToConsole.execute();
    } else {
      inputAnimator_.enque(event.getCode(), finishSendToConsole);
    }
  }
Esempio n. 5
0
  public String processCommandEntry() {
    // parse out the command text
    String promptText = prompt_.getElement().getInnerText();
    String commandText = input_.getCode();
    input_.setText("");
    // Force render to avoid subtle command movement in the console, caused
    // by the prompt disappearing before the input line does
    input_.forceImmediateRender();
    prompt_.setHTML("");

    SpanElement pendingPrompt = Document.get().createSpanElement();
    pendingPrompt.setInnerText(promptText);
    pendingPrompt.setClassName(styles_.prompt() + " " + KEYWORD_CLASS_NAME);

    if (!suppressPendingInput_ && !input_.isPasswordMode()) {
      SpanElement pendingInput = Document.get().createSpanElement();
      pendingInput.setInnerText(StringUtil.notNull(commandText).split("\n")[0] + "\n");
      pendingInput.setClassName(styles_.command() + " " + KEYWORD_CLASS_NAME);
      pendingInput_.getElement().appendChild(pendingPrompt);
      pendingInput_.getElement().appendChild(pendingInput);
      pendingInput_.setVisible(true);
    }

    ensureInputVisible();

    return commandText;
  }
Esempio n. 6
0
  public void setCode(String code, boolean preserveCursorPosition) {
    // Calling setCode("", false) while the editor contains multiple lines of
    // content causes bug 2928: Flickering console when typing. Empirically,
    // first setting code to a single line of content and then clearing it,
    // seems to correct this problem.
    if (StringUtil.isNullOrEmpty(code)) doSetCode(" ", preserveCursorPosition);

    doSetCode(code, preserveCursorPosition);
  }
  @Override
  public void onRPubsPublishStatus(RPubsUploadStatusEvent event) {
    // make sure it applies to our context
    RPubsUploadStatusEvent.Status status = event.getStatus();

    if (StringUtil.isNullOrEmpty(status.getError())) {
      populateDeployments(true);
    }
  }
Esempio n. 8
0
 public void setText(String label) {
   if (!StringUtil.isNullOrEmpty(label)) {
     label_.setInnerText(label);
     label_.getStyle().setDisplay(Display.BLOCK);
     removeStyleName(styles_.noLabel());
   } else {
     label_.getStyle().setDisplay(Display.NONE);
     addStyleName(styles_.noLabel());
   }
 }
Esempio n. 9
0
  public void consolePrompt(String prompt, boolean showInput) {
    if (prompt != null) prompt = VirtualConsole.consolify(prompt);

    prompt_.getElement().setInnerText(prompt);
    // input_.clear() ;
    ensureInputVisible();

    // Deal gracefully with multi-line prompts
    int promptLines = StringUtil.notNull(prompt).split("\\n").length;
    input_.asWidget().getElement().getStyle().setPaddingTop((promptLines - 1) * 15, Unit.PX);

    input_.setPasswordMode(!showInput);
  }
Esempio n. 10
0
  private String makeCommand(ImportFileSettings input) {
    HashMap<String, ImportFileSettings> commandDefaults_ =
        new HashMap<String, ImportFileSettings>();

    commandDefaults_.put("read.table", new ImportFileSettings(null, null, false, "", ".", "\"'"));
    commandDefaults_.put("read.csv", new ImportFileSettings(null, null, true, ",", ".", "\""));
    commandDefaults_.put("read.delim", new ImportFileSettings(null, null, true, "\t", ".", "\""));
    commandDefaults_.put("read.csv2", new ImportFileSettings(null, null, true, ";", ",", "\""));
    commandDefaults_.put("read.delim2", new ImportFileSettings(null, null, true, "\t", ",", "\""));

    String command = "read.table";
    ImportFileSettings settings = commandDefaults_.get("read.table");
    int score = settings.calculateSimilarity(input);
    for (String cmd : new String[] {"read.csv", "read.delim"}) {
      ImportFileSettings theseSettings = commandDefaults_.get(cmd);
      int thisScore = theseSettings.calculateSimilarity(input);
      if (thisScore > score) {
        score = thisScore;
        command = cmd;
        settings = theseSettings;
      }
    }

    StringBuilder code = new StringBuilder(command);
    code.append("(");
    code.append(StringUtil.textToRLiteral(input.getFile().getPath()));
    if (input.isHeader() != settings.isHeader())
      code.append(", header=" + (input.isHeader() ? "T" : "F"));
    if (!input.getSep().equals(settings.getSep()))
      code.append(", sep=" + StringUtil.textToRLiteral(input.getSep()));
    if (!input.getDec().equals(settings.getDec()))
      code.append(", dec=" + StringUtil.textToRLiteral(input.getDec()));
    if (!input.getQuote().equals(settings.getQuote()))
      code.append(", quote=" + StringUtil.textToRLiteral(input.getQuote()));
    code.append(")");

    return code.toString();
  }
Esempio n. 11
0
  public static int parseDisableModes(String disableModes) {
    int mode = KeyboardShortcut.MODE_NONE;

    if (StringUtil.isNullOrEmpty(disableModes)) return mode;

    String[] splat = disableModes.split(",");
    for (String item : splat) {
      if (item.equals("default")) mode |= KeyboardShortcut.MODE_DEFAULT;
      else if (item.equals("vim")) mode |= KeyboardShortcut.MODE_VIM;
      else if (item.equals("emacs")) mode |= KeyboardShortcut.MODE_EMACS;
      else assert false : "Unrecognized 'disableModes' value '" + item + "'";
    }

    return mode;
  }
  private String createPrefix() {
    StringBuilder builder = new StringBuilder();
    String title = txtTitle_.getValue().trim();
    if (title.length() > 0) {
      builder.append("### ").append(SafeHtmlUtils.htmlEscape(title)).append("\n");
    }

    String author = txtAuthor_.getValue().trim();
    if (author.length() > 0) {
      builder.append(SafeHtmlUtils.htmlEscape(author)).append(" --- ");
    }
    builder.append("*");
    builder.append(StringUtil.formatDate(new Date()));
    builder.append("*");

    return builder.toString();
  }
Esempio n. 13
0
 @Override
 public void onRmdRenderCompleted(RmdRenderCompletedEvent event) {
   // ensure we got a result--note that even a cancelled render generates an
   // event, but with an empty output file
   if (rmdRenderPending_
       && event.getResult() != null
       && !StringUtil.isNullOrEmpty(event.getResult().getOutputFile())) {
     RenderedDocPreview docPreview = new RenderedDocPreview(event.getResult());
     events_.fireEvent(
         RSConnectActionEvent.DeployDocEvent(
             docPreview,
             event.getResult().isWebsite()
                 ? RSConnect.CONTENT_TYPE_WEBSITE
                 : RSConnect.CONTENT_TYPE_DOCUMENT,
             publishAfterRmdRender_));
   }
   publishAfterRmdRender_ = null;
   rmdRenderPending_ = false;
   anyRmdRenderPending_ = false;
 }
  @Override
  public void onRSConnectDeploymentStarted(RSConnectDeploymentStartedEvent event) {
    switchToConsoleAfterDeploy_ = !view_.isEffectivelyVisible();
    view_.ensureVisible(true);

    // show the filename in the deployment tab, unless we're deploying an
    // HTML file for which we know the title--this may very well be a
    // temporary file
    String title = event.getPath();
    if (title == null) title = "";

    if ((title.isEmpty()
        || title.toLowerCase().endsWith(".htm")
        || title.toLowerCase().endsWith(".html") && !StringUtil.isNullOrEmpty(event.getTitle()))) {
      title = event.getTitle();
    }

    view_.compileStarted(title);
    setIsBusy(true);
  }
 public boolean hasDocOutput() {
   return originatingEvent_ != null
       && originatingEvent_.getFromPreview() != null
       && !StringUtil.isNullOrEmpty(originatingEvent_.getFromPreview().getOutputFile());
 }
Esempio n. 16
0
 public String getText() {
   return StringUtil.notNull(label_.getInnerText());
 }
Esempio n. 17
0
    public void endDrag(final Event evt, int action) {
      if (curState_ == STATE_NONE) return;

      // remove the properties used to position for dragging
      if (dragElement_ != null) {
        dragElement_.getStyle().clearLeft();
        dragElement_.getStyle().clearPosition();
        dragElement_.getStyle().clearZIndex();
        dragElement_.getStyle().clearDisplay();
        dragElement_.getStyle().clearOpacity();

        // insert this tab where the placeholder landed if we're not
        // cancelling
        if (action == ACTION_COMMIT) {
          dragTabsHost_.removeChild(dragElement_);
          dragTabsHost_.insertAfter(dragElement_, dragPlaceholder_);
        }
      }

      // remove the placeholder
      if (dragPlaceholder_ != null) {
        dragTabsHost_.removeChild(dragPlaceholder_);
        dragPlaceholder_ = null;
      }

      if (dragElement_ != null && action == ACTION_EXTERNAL) {
        // if we own the dragged tab, change to external drag state
        dragElement_.getStyle().setOpacity(0.4);
        curState_ = STATE_EXTERNAL;
      } else {
        // otherwise, we're back to pristine
        curState_ = STATE_NONE;
        events_.fireEvent(new DocTabDragStateChangedEvent(DocTabDragStateChangedEvent.STATE_NONE));
      }

      if (dragElement_ != null && action == ACTION_COMMIT) {
        // let observer know we moved; adjust the destination position one to
        // the left if we're right of the start position to account for the
        // position of the tab prior to movement
        if (startPos_ != null && startPos_ != destPos_) {
          TabReorderEvent event = new TabReorderEvent(startPos_, destPos_);
          fireEvent(event);
        }
      }

      // this is the case when we adopt someone else's doc
      if (dragElement_ == null && evt != null && action == ACTION_COMMIT) {
        // pull the document ID and source window out
        String data = evt.getDataTransfer().getData(getDataTransferFormat());
        if (StringUtil.isNullOrEmpty(data)) return;

        // the data format is docID|windowID; windowID can be omitted if
        // the main window is the origin
        String pieces[] = data.split("\\|");
        if (pieces.length < 1) return;

        events_.fireEvent(
            new DocWindowChangedEvent(
                pieces[0], pieces.length > 1 ? pieces[1] : "", initDragParams_, destPos_));
      }

      // this is the case when our own drag ends; if it ended outside our
      // window and outside all satellites, treat it as a tab tear-off
      if (dragElement_ != null && evt != null && action == ACTION_CANCEL) {
        // if this is the last tab in satellite, we don't want to tear
        // it out
        boolean isLastSatelliteTab = docTabs_.size() == 1 && Satellite.isCurrentWindowSatellite();

        // did the user drag the tab outside this doc?
        if (!isLastSatelliteTab
            && DomUtils.elementFromPoint(evt.getClientX(), evt.getClientY()) == null) {
          // did it end in any RStudio satellite window?
          String targetWindowName;
          Satellite satellite = RStudioGinjector.INSTANCE.getSatellite();
          if (Satellite.isCurrentWindowSatellite()) {
            // this is a satellite, ask the main window
            targetWindowName = satellite.getWindowAtPoint(evt.getScreenX(), evt.getScreenY());
          } else {
            // this is the main window, query our own satellites
            targetWindowName =
                RStudioGinjector.INSTANCE
                    .getSatelliteManager()
                    .getWindowAtPoint(evt.getScreenX(), evt.getScreenY());
          }
          if (targetWindowName == null) {
            // it was dragged over nothing RStudio owns--pop it out
            events_.fireEvent(
                new PopoutDocInitiatedEvent(
                    initDragParams_.getDocId(), new Point(evt.getScreenX(), evt.getScreenY())));
          }
        }
      }

      if (curState_ != STATE_EXTERNAL) {
        // if we're in an end state, clear the drag element
        dragElement_ = null;
      }
    }
Esempio n. 18
0
 public final boolean isVcsEnabled() {
   return !StringUtil.isNullOrEmpty(getVcsName());
 }
Esempio n. 19
0
 private void onPublishRecordClick(final RSConnectDeploymentRecord previous) {
   switch (contentType_) {
     case RSConnect.CONTENT_TYPE_HTML:
     case RSConnect.CONTENT_TYPE_PRES:
       if (publishHtmlSource_ == null) {
         display_.showErrorMessage(
             "Content Publish Failed", "No HTML could be generated for the content.");
         return;
       }
       publishHtmlSource_.generatePublishHtml(
           new CommandWithArg<String>() {
             @Override
             public void execute(String arg) {
               events_.fireEvent(
                   RSConnectActionEvent.DeployHtmlEvent(
                       contentType_, contentPath_, arg, publishHtmlSource_.getTitle(), previous));
             }
           });
       break;
     case RSConnect.CONTENT_TYPE_PLOT:
       // for plots, we need to generate the hosting HTML prior to publishing
       if (publishHtmlSource_ != null) {
         publishHtmlSource_.generatePublishHtml(
             new CommandWithArg<String>() {
               @Override
               public void execute(String htmlFile) {
                 events_.fireEvent(RSConnectActionEvent.DeployPlotEvent(htmlFile, previous));
               }
             });
       }
       break;
     case RSConnect.CONTENT_TYPE_APP:
     case RSConnect.CONTENT_TYPE_APP_SINGLE:
       // Shiny application
       events_.fireEvent(
           RSConnectActionEvent.DeployAppEvent(contentPath_, contentType_, previous));
       break;
     case RSConnect.CONTENT_TYPE_DOCUMENT:
       if (docPreview_ == null
           || (docPreview_.isStatic()
               && StringUtil.isNullOrEmpty(docPreview_.getOutputFile())
               && docPreview_.getSourceFile() != null)) {
         // if the doc has been saved but not been rendered, go render it and
         // come back when we're finished
         renderThenPublish(contentPath_, previous);
       } else {
         // All R Markdown variants (single/multiple and static/Shiny)
         if (docPreview_.getSourceFile() == null) {
           display_.showErrorMessage(
               "Unsaved Document",
               "Unsaved documents cannot be published. Save the document "
                   + "before publishing it.");
           break;
         }
         events_.fireEvent(
             RSConnectActionEvent.DeployDocEvent(
                 docPreview_, RSConnect.CONTENT_TYPE_DOCUMENT, previous));
       }
       break;
     case RSConnect.CONTENT_TYPE_WEBSITE:
       events_.fireEvent(
           RSConnectActionEvent.DeployDocEvent(
               docPreview_, RSConnect.CONTENT_TYPE_WEBSITE, previous));
       break;
     default:
       // should never happen
       display_.showErrorMessage(
           "Can't Publish " + RSConnect.contentTypeDesc(contentType_),
           "The content type '"
               + RSConnect.contentTypeDesc(contentType_)
               + "' is not currently supported for publishing.");
   }
 }
Esempio n. 20
0
 private void executeFunctionForObject(String function, String objectName) {
   String editCode = function + "(" + StringUtil.toRSymbolName(objectName) + ")";
   SendToConsoleEvent event = new SendToConsoleEvent(editCode, true);
   eventBus_.fireEvent(event);
 }
Esempio n. 21
0
 public String getPromptText() {
   return StringUtil.notNull(prompt_.getText());
 }
Esempio n. 22
0
 public boolean isPromptEmpty() {
   return StringUtil.isNullOrEmpty(prompt_.getText());
 }
Esempio n. 23
0
  // rebuilds the popup menu--this can happen when the menu is invoked; it can
  // also happen when the button is created if we're aggressively checking
  // publish status
  private void rebuildPopupMenu(final ToolbarPopupMenu.DynamicPopupMenuCallback callback) {
    final ToolbarPopupMenu menu = publishMenu_;

    // prevent reentrancy
    if (populating_) {
      if (callback != null) callback.onPopupMenu(menu);
      return;
    }

    // handle case where we don't have a content path (i.e. plots)
    if (contentPath_ == null) {
      setPreviousDeployments(null);
      if (callback != null) callback.onPopupMenu(menu);
      return;
    }

    // avoid populating if we've already set the deployments for this path
    // (unless we're forcefully repopulating)
    if (populatedPath_ != null && populatedPath_.equals(contentPath_)) {
      if (callback != null) callback.onPopupMenu(menu);
      return;
    }

    String contentPath = contentPath_;
    boolean parent = false;

    // if this is a Shiny application and an .R file is being invoked, check
    // for deployments of its parent path (single-file apps have
    // CONTENT_TYPE_APP_SINGLE and their own deployment records)
    if (contentType_ == RSConnect.CONTENT_TYPE_APP
        && StringUtil.getExtension(contentPath_).equalsIgnoreCase("r")) parent = true;

    // if this is a document in a website, use the parent path
    if (contentType_ == RSConnect.CONTENT_TYPE_WEBSITE) parent = true;

    // apply parent path if needed
    if (parent) {
      FileSystemItem fsiContent = FileSystemItem.createFile(contentPath_);
      contentPath = fsiContent.getParentPathString();
    }

    populating_ = true;
    server_.getRSConnectDeployments(
        contentPath,
        outputPath_ == null ? "" : outputPath_,
        new ServerRequestCallback<JsArray<RSConnectDeploymentRecord>>() {
          @Override
          public void onResponseReceived(JsArray<RSConnectDeploymentRecord> recs) {
            populatedPath_ = contentPath_;
            populating_ = false;

            // if publishing a website but not content, filter deployments
            // that are static (as we can't update them)
            if (contentType_ == RSConnect.CONTENT_TYPE_WEBSITE
                && (docPreview_ == null || StringUtil.isNullOrEmpty(docPreview_.getOutputFile()))) {
              JsArray<RSConnectDeploymentRecord> codeRecs = JsArray.createArray().cast();
              for (int i = 0; i < recs.length(); i++) {
                if (!recs.get(i).getAsStatic()) codeRecs.push(recs.get(i));
              }
              recs = codeRecs;
            }

            setPreviousDeployments(recs);
            if (callback != null) callback.onPopupMenu(menu);
          }

          @Override
          public void onError(ServerError error) {
            populating_ = false;
            if (callback != null) callback.onPopupMenu(menu);
          }
        });
  }
Esempio n. 24
0
  private boolean output(String text, String className, boolean addToTop) {
    if (text.indexOf('\f') >= 0) clearOutput();

    Node node;
    boolean isOutput = StringUtil.isNullOrEmpty(className) || className.equals(styles_.output());

    if (isOutput && !addToTop && trailingOutput_ != null) {
      // Short-circuit the case where we're appending output to the
      // bottom, and there's already some output there. We need to
      // treat this differently in case the new output uses control
      // characters to pound over parts of the previous output.

      int oldLineCount = DomUtils.countLines(trailingOutput_, true);
      trailingOutputConsole_.submit(text);
      trailingOutput_.setNodeValue(ensureNewLine(trailingOutputConsole_.toString()));
      int newLineCount = DomUtils.countLines(trailingOutput_, true);
      lines_ += newLineCount - oldLineCount;
    } else {
      Element outEl = output_.getElement();

      text = VirtualConsole.consolify(text);
      if (isOutput) {
        VirtualConsole console = new VirtualConsole();
        console.submit(text);
        String consoleSnapshot = console.toString();

        // We use ensureNewLine to make sure that even if output
        // doesn't end with \n, a prompt will appear on its own line.
        // However, if we call ensureNewLine indiscriminantly (i.e.
        // on an output that's going to be followed by another output)
        // we can end up inserting newlines where they don't belong.
        //
        // It's safe to add a newline when we're appending output to
        // the end of the console, because if the next append is also
        // output, we'll use the contents of VirtualConsole and the
        // newline we add here will be plowed over.
        //
        // If we're prepending output to the top of the console, then
        // it's safe to add a newline if the next chunk (which is already
        // there) is something besides output.
        if (!addToTop
            || (!outEl.hasChildNodes() || outEl.getFirstChild().getNodeType() != Node.TEXT_NODE)) {
          consoleSnapshot = ensureNewLine(consoleSnapshot);
        }

        node = Document.get().createTextNode(consoleSnapshot);
        if (!addToTop) {
          trailingOutput_ = (Text) node;
          trailingOutputConsole_ = console;
        }
      } else {
        SpanElement span = Document.get().createSpanElement();
        span.setClassName(className);
        span.setInnerText(text);
        node = span;
        if (!addToTop) {
          trailingOutput_ = null;
          trailingOutputConsole_ = null;
        }
      }

      if (addToTop) outEl.insertFirst(node);
      else outEl.appendChild(node);

      lines_ += DomUtils.countLines(node, true);
    }
    return !trimExcess();
  }