/* * RmdOutput.java * * Copyright (C) 2009-14 by RStudio, Inc. * * Unless you have received this program directly from RStudio pursuant * to the terms of a commercial license agreement with RStudio, then * this program is licensed to you under the terms of version 3 of the * GNU Affero General Public License. This program is distributed WITHOUT * ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT, * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the * AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details. * */ package org.rstudio.studio.client.rmarkdown; import java.util.Map; import java.util.HashMap; import org.rstudio.core.client.command.CommandBinder; import org.rstudio.core.client.dom.WindowEx; import org.rstudio.core.client.files.FileSystemItem; import org.rstudio.core.client.widget.Operation; import org.rstudio.core.client.widget.OperationWithInput; import org.rstudio.core.client.widget.ProgressIndicator; import org.rstudio.core.client.widget.ProgressOperation; import org.rstudio.studio.client.RStudioGinjector; import org.rstudio.studio.client.application.Desktop; import org.rstudio.studio.client.application.events.EventBus; import org.rstudio.studio.client.application.events.QuitInitiatedEvent; import org.rstudio.studio.client.application.events.QuitInitiatedHandler; import org.rstudio.studio.client.application.events.RestartStatusEvent; import org.rstudio.studio.client.common.GlobalDisplay; import org.rstudio.studio.client.common.SimpleRequestCallback; import org.rstudio.studio.client.common.filetypes.FileTypeRegistry; import org.rstudio.studio.client.common.filetypes.TextFileType; import org.rstudio.studio.client.common.viewfile.ViewFilePanel; import org.rstudio.studio.client.pdfviewer.PDFViewer; import org.rstudio.studio.client.rmarkdown.events.ConvertToShinyDocEvent; import org.rstudio.studio.client.rmarkdown.events.PreviewRmdEvent; import org.rstudio.studio.client.rmarkdown.events.RenderRmdEvent; import org.rstudio.studio.client.rmarkdown.events.RenderRmdSourceEvent; import org.rstudio.studio.client.rmarkdown.events.RmdRenderCompletedEvent; import org.rstudio.studio.client.rmarkdown.events.RmdRenderStartedEvent; import org.rstudio.studio.client.rmarkdown.events.RmdShinyDocStartedEvent; import org.rstudio.studio.client.rmarkdown.events.RmdRenderPendingEvent; import org.rstudio.studio.client.rmarkdown.events.WebsiteFileSavedEvent; import org.rstudio.studio.client.rmarkdown.model.RMarkdownServerOperations; import org.rstudio.studio.client.rmarkdown.model.RmdEditorOptions; import org.rstudio.studio.client.rmarkdown.model.RmdOutputFormat; import org.rstudio.studio.client.rmarkdown.model.RmdPreviewParams; import org.rstudio.studio.client.rmarkdown.model.RmdRenderResult; import org.rstudio.studio.client.rmarkdown.model.RmdShinyDocInfo; import org.rstudio.studio.client.rmarkdown.ui.RmdOutputFrame; import org.rstudio.studio.client.rmarkdown.ui.ShinyDocumentWarningDialog; import org.rstudio.studio.client.rsconnect.ui.RSConnectPublishButton; import org.rstudio.studio.client.server.ServerError; import org.rstudio.studio.client.server.ServerRequestCallback; import org.rstudio.studio.client.server.VoidServerRequestCallback; import org.rstudio.studio.client.server.Void; import org.rstudio.studio.client.workbench.WorkbenchContext; import org.rstudio.studio.client.workbench.commands.Commands; import org.rstudio.studio.client.workbench.model.Session; import org.rstudio.studio.client.workbench.prefs.events.UiPrefsChangedEvent; import org.rstudio.studio.client.workbench.prefs.events.UiPrefsChangedHandler; import org.rstudio.studio.client.workbench.prefs.model.UIPrefs; import org.rstudio.studio.client.workbench.views.source.SourceBuildHelper; import org.rstudio.studio.client.workbench.views.source.events.NotebookRenderFinishedEvent; import com.google.gwt.core.client.JavaScriptObject; import com.google.gwt.core.client.Scheduler; import com.google.gwt.core.client.Scheduler.ScheduledCommand; import com.google.gwt.event.logical.shared.ValueChangeEvent; import com.google.gwt.event.logical.shared.ValueChangeHandler; import com.google.gwt.user.client.Command; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; @Singleton public class RmdOutput implements RmdRenderStartedEvent.Handler, RmdRenderCompletedEvent.Handler, RmdShinyDocStartedEvent.Handler, PreviewRmdEvent.Handler, RenderRmdEvent.Handler, RenderRmdSourceEvent.Handler, RestartStatusEvent.Handler, WebsiteFileSavedEvent.Handler, NotebookRenderFinishedEvent.Handler, RmdRenderPendingEvent.Handler, QuitInitiatedHandler, UiPrefsChangedHandler { public interface Binder extends CommandBinder<Commands, RmdOutput> {} @Inject public RmdOutput(EventBus eventBus, Commands commands, Session session, GlobalDisplay globalDisplay, FileTypeRegistry fileTypeRegistry, SourceBuildHelper sourceBuildHelper, WorkbenchContext workbenchContext, Provider<ViewFilePanel> pViewFilePanel, Binder binder, UIPrefs prefs, PDFViewer pdfViewer, RMarkdownServerOperations server) { globalDisplay_ = globalDisplay; fileTypeRegistry_ = fileTypeRegistry; sourceBuildHelper_ = sourceBuildHelper; workbenchContext_ = workbenchContext; pViewFilePanel_ = pViewFilePanel; prefs_ = prefs; pdfViewer_ = pdfViewer; server_ = server; events_ = eventBus; session_ = session; commands_ = commands; eventBus.addHandler(RmdRenderStartedEvent.TYPE, this); eventBus.addHandler(RmdRenderCompletedEvent.TYPE, this); eventBus.addHandler(RmdShinyDocStartedEvent.TYPE, this); eventBus.addHandler(PreviewRmdEvent.TYPE, this); eventBus.addHandler(RenderRmdEvent.TYPE, this); eventBus.addHandler(RenderRmdSourceEvent.TYPE, this); eventBus.addHandler(RestartStatusEvent.TYPE, this); eventBus.addHandler(UiPrefsChangedEvent.TYPE, this); eventBus.addHandler(WebsiteFileSavedEvent.TYPE, this); eventBus.addHandler(QuitInitiatedEvent.TYPE, this); eventBus.addHandler(RmdRenderPendingEvent.TYPE, this); eventBus.addHandler(NotebookRenderFinishedEvent.TYPE, this); prefs_.rmdViewerType().addValueChangeHandler(new ValueChangeHandler<Integer>() { @Override public void onValueChange(ValueChangeEvent<Integer> e) { onViewerTypeChanged(e.getValue()); } }); binder.bind(commands, this); exportRmdOutputClosedCallback(); } @Override public void onRmdRenderPending(RmdRenderPendingEvent event) { renderInProgress_ = true; } @Override public void onRmdRenderStarted(RmdRenderStartedEvent event) { // When a Word document starts rendering, tell the desktop frame // (if it exists) to get ready; this generally involves closing the // document in preparation for a refresh if (event.getFormat().getFormatName() .equals(RmdOutputFormat.OUTPUT_WORD_DOCUMENT) && Desktop.isDesktop()) { Desktop.getFrame().prepareShowWordDoc(); } } @Override public void onRmdRenderCompleted(RmdRenderCompletedEvent event) { renderInProgress_ = false; // if there's a custom operation to be run when render completes, run // that instead if (onRenderCompleted_ != null) { onRenderCompleted_.execute(); onRenderCompleted_ = null; return; } // ignore failures and completed Shiny docs (the latter are handled when // the server starts rather than when the render process is finished) final RmdRenderResult result = event.getResult(); if (result.isShinyDocument()) { shinyDoc_ = null; return; } if (result.hasShinyContent() && !result.isShinyDocument()) { // If the result has Shiny content but wasn't rendered as a Shiny // document, suggest rendering as a Shiny document instead new ShinyDocumentWarningDialog(new OperationWithInput<Integer>() { @Override public void execute(Integer input) { switch (input) { case ShinyDocumentWarningDialog.RENDER_SHINY_NO: if (result.getSucceeded()) displayRenderResult(result); break; case ShinyDocumentWarningDialog.RENDER_SHINY_ONCE: rerenderAsShiny(result); break; case ShinyDocumentWarningDialog.RENDER_SHINY_ALWAYS: events_.fireEvent(new ConvertToShinyDocEvent (result.getTargetFile())); break; } } }).showModal(); } else if (result.getSucceeded()) { displayRenderResult(event.getResult()); } } @Override public void onRmdShinyDocStarted(RmdShinyDocStartedEvent event) { shinyDoc_ = event.getDocInfo(); RmdRenderResult result = RmdRenderResult.createFromShinyDoc(shinyDoc_); displayHTMLRenderResult(result); } @Override public void onPreviewRmd(final PreviewRmdEvent event) { RenderRmdEvent renderEvent = new RenderRmdEvent( event.getSourceFile(), 1, null, event.getEncoding(), null, false, RmdOutput.TYPE_STATIC, event.getOutputFile(), null, null); events_.fireEvent(renderEvent); } @Override public void onRenderRmd(final RenderRmdEvent event) { quitInitiatedAfterLastRender_ = false; final Operation renderOperation = new Operation() { @Override public void execute() { renderInProgress_ = true; server_.renderRmd(event.getSourceFile(), event.getSourceLine(), event.getFormat(), event.getEncoding(), event.getParamsFile(), event.asTempfile(), event.getType(), event.getExistingOutputFile(), event.getWorkingDir(), event.getViewerType(), new SimpleRequestCallback<Boolean>() { @Override public void onError(ServerError error) { renderInProgress_ = false; } }); } }; // If there's a running shiny document for this file and it's not a // presentation, we can do an in-place reload. Note that we don't // currently support in-place reload for Shiny presentations since we // would need to hook a client event at the end of the re-render that // emitted updated slide navigation information and then plumbed that // information back into the preview window. if (shinyDoc_ != null && event.getSourceFile().equals(shinyDoc_.getFile()) && !shinyDoc_.getFormat().getFormatName().endsWith( RmdOutputFormat.OUTPUT_PRESENTATION_SUFFIX) && (result_ == null || "shiny".equals(result_.getRuntime()))) { final RmdRenderResult result = RmdRenderResult.createFromShinyDoc(shinyDoc_); displayHTMLRenderResult(result); } else { performRenderOperation(renderOperation); } } @Override public void onNotebookRenderFinished(NotebookRenderFinishedEvent event) { // ignore if no result, no output frame/closed output frame, or frame not // associated with this document if (result_ == null || outputFrame_ == null || outputFrame_.getWindowObject() == null || outputFrame_.getWindowObject().isClosed() || outputFrame_.getPreviewParams().getTargetFile() != event.getDocPath() || !outputFrame_.getPreviewParams().getOutputFile().endsWith(".nb.html")) return; // redisplay the result displayRenderResult(result_); } @Override public void onWebsiteFileSaved(WebsiteFileSavedEvent event) { // auto reload/rerender on file saves (first apply various // filters to not auto reload). note that before we even // receive this event we know the file is one that is contained // in the website directory // skip if there is a build in progress if (workbenchContext_.isBuildInProgress()) return; // skip if there is a render in progress if (renderInProgress_) return; // skip if there was a quit initiated since the last render if (quitInitiatedAfterLastRender_) return; // is there an output frame? if (outputFrame_ == null || outputFrame_.getWindowObject() == null) return; // is it showing a page from the current site? String websiteDir = session_.getSessionInfo().getBuildTargetDir(); final RmdPreviewParams params = outputFrame_.getPreviewParams(); if (!params.getTargetFile().startsWith(websiteDir)) return; // is the changed file one that should always produce a rebuild? FileSystemItem file = event.getFileSystemItem(); TextFileType fileType = fileTypeRegistry_.getTextTypeForFile(file); String typeId = fileType.getTypeId(); if (fileType.isR() || typeId.equals(FileTypeRegistry.HTML.getTypeId()) || typeId.equals(FileTypeRegistry.YAML.getTypeId()) || typeId.equals(FileTypeRegistry.JSON.getTypeId())) { reRenderPreview(); } // is the changed file a markdown document else if (fileType.isMarkdown()) { // included Rmd files always produce a rebuild of the current file if (file.getStem().startsWith("_")) reRenderPreview(); // files in subdirectories are also includes so re-render them also if (!file.getParentPathString().equals(websiteDir)) reRenderPreview(); // ...otherwise leave it alone (requires a knit) } // see if this should result in a copy + refresh else { server_.maybeCopyWebsiteAsset(file.getPath(), new SimpleRequestCallback<Boolean>() { @Override public void onResponseReceived(Boolean copied) { if (copied) outputFrame_.showRmdPreview(params, true); } }); } } private void reRenderPreview() { reRenderPreview(null); } private void reRenderPreview(String targetFile) { if (outputFrame_ == null) return; livePreviewRenderInProgress_ = true; RmdPreviewParams params = outputFrame_.getPreviewParams(); if (targetFile == null) targetFile = params.getTargetFile(); RenderRmdEvent renderEvent = new RenderRmdEvent( targetFile, 1, params.getResult().getFormatName(), params.getResult().getTargetEncoding(), null, false, RmdOutput.TYPE_STATIC, null, null, null); events_.fireEvent(renderEvent); } @Override public void onQuitInitiated(QuitInitiatedEvent event) { quitInitiatedAfterLastRender_ = true; } @Override public void onRenderRmdSource(final RenderRmdSourceEvent event) { quitInitiatedAfterLastRender_ = false; performRenderOperation(new Operation() { @Override public void execute() { server_.renderRmdSource(event.getSource(), new SimpleRequestCallback<Boolean>()); } }); } @Override public void onRestartStatus(RestartStatusEvent event) { // preemptively close the satellite window when R restarts (so we don't // wait around if the session doesn't get a chance to tell us about // terminated renders) if (event.getStatus() == RestartStatusEvent.RESTART_INITIATED) { if (outputFrame_ != null) outputFrame_.closeOutputFrame(false); restarting_ = true; } else { restarting_ = false; } } @Override public void onUiPrefsChanged(UiPrefsChangedEvent e) { onViewerTypeChanged(prefs_.rmdViewerType().getValue()); } // Private methods --------------------------------------------------------- private void onViewerTypeChanged(int newViewerType) { if (outputFrame_ != null && outputFrame_.getWindowObject() != null && newViewerType != outputFrame_.getViewerType()) { // close the existing frame RmdPreviewParams params = outputFrame_.getPreviewParams(); outputFrame_.closeOutputFrame(true); // reset the scroll position (as it will vary with the document width, // which will change) params.setScrollPosition(0); // open a new one with the same parameters outputFrame_ = createOutputFrame(newViewerType); outputFrame_.showRmdPreview(params, true); } else if (outputFrame_ != null && outputFrame_.getWindowObject() == null && outputFrame_.getViewerType() != newViewerType) { // output frame exists but doesn't have a loaded doc, clear it so we'll // create the frame appropriate to this type on next render outputFrame_ = null; } } // perform the given render after terminating the currently running Shiny // application if there is one private void performRenderOperation(final Operation renderOperation) { if (shinyDoc_ != null) { // if we already have this up in the viewer pane, cache the scroll // position (we don't need to do this for the satellite since it // caches scroll position when it closes) if (result_ != null && outputFrame_.getViewerType() == RMD_VIEWER_TYPE_PANE) { cacheDocPosition(result_, outputFrame_.getScrollPosition(), outputFrame_.getAnchor()); } // there is a Shiny doc running; we'll need to terminate it before // we can render this document outputFrame_.closeOutputFrame(false); server_.terminateRenderRmd(true, new ServerRequestCallback<Void>() { @Override public void onResponseReceived(Void v) { onRenderCompleted_ = renderOperation; shinyDoc_ = null; } @Override public void onError(ServerError error) { globalDisplay_.showErrorMessage("Shiny Terminate Failed", "The Shiny document " + shinyDoc_.getFile() + " needs to " + "be stopped before the document can be rendered."); } }); } else { renderOperation.execute(); } } private void rerenderAsShiny(RmdRenderResult result) { events_.fireEvent(new RenderRmdEvent( result.getTargetFile(), result.getTargetLine(), null, result.getTargetEncoding(), null, false, RmdOutput.TYPE_SHINY, null, null, result.getViewerType())); } private void displayRenderResult(final RmdRenderResult result) { // don't display anything if user doesn't want to if (prefs_.rmdViewerType().getValue() == RMD_VIEWER_TYPE_NONE) return; String extension = FileSystemItem.getExtensionFromPath( result.getOutputFile()); if (".pdf".equals(extension)) { String previewer = prefs_.pdfPreview().getValue(); if (previewer.equals(UIPrefs.PDF_PREVIEW_RSTUDIO)) { pdfViewer_.viewPdfUrl( result.getOutputUrl(), result.getPreviewSlide() >= 0 ? result.getPreviewSlide() : null); } else if (!previewer.equals(UIPrefs.PDF_PREVIEW_NONE)) { if (Desktop.isDesktop()) Desktop.getFrame().showPDF(result.getOutputFile(), result.getPreviewSlide()); else globalDisplay_.showHtmlFile(result.getOutputFile()); } } else if (".docx".equals(extension) || ".rtf".equals(extension) || ".odt".equals(extension)) { if (Desktop.isDesktop()) globalDisplay_.showWordDoc(result.getOutputFile()); // it's not possible to show Word docs inline in a useful way from // within the browser, so just offer to download the file. else { showDownloadPreviewFileDialog(result, new Command() { @Override public void execute() { globalDisplay_.showWordDoc(result.getOutputFile()); } }); } } else if (".html".equals(extension) || NOTEBOOK_EXT.equals(extension)) { displayHTMLRenderResult(result); } else if (".md".equalsIgnoreCase(extension) || extension.toLowerCase().startsWith(".markdown") || ".tex".equalsIgnoreCase(extension)) { ViewFilePanel viewFilePanel = pViewFilePanel_.get(); viewFilePanel.showFile( FileSystemItem.createFile(result.getOutputFile()), "UTF-8"); } else { if (Desktop.isDesktop()) Desktop.getFrame().showFile(result.getOutputFile()); else { showDownloadPreviewFileDialog(result, new Command() { @Override public void execute() { String url = server_.getFileUrl( FileSystemItem.createFile(result.getOutputFile())); globalDisplay_.openWindow(url); } }); } } } private void showDownloadPreviewFileDialog( final RmdRenderResult result, final Command onDownload) { globalDisplay_.showYesNoMessage(GlobalDisplay.MSG_INFO, "R Markdown Render Completed", "R Markdown has finished rendering " + result.getTargetFile() + " to " + result.getOutputFile() + ".", false, new ProgressOperation() { @Override public void execute(ProgressIndicator indicator) { onDownload.execute(); indicator.onCompleted(); } }, null, "Download File", "OK", false); } private void displayHTMLRenderResult(RmdRenderResult result) { // find the last known position for this file int scrollPosition = 0; String anchor = ""; if (scrollPositions_.containsKey(keyFromResult(result))) { scrollPosition = scrollPositions_.get(keyFromResult(result)); } if (anchors_.containsKey(keyFromResult(result))) { anchor = anchors_.get(keyFromResult(result)); } final RmdPreviewParams params = RmdPreviewParams.create( result, scrollPosition, anchor); // get the default viewer type from prefs int viewerType = prefs_.rmdViewerType().getValue(); // apply override from result, if any if (result.getViewerType() == RmdEditorOptions.PREVIEW_IN_VIEWER) viewerType = RMD_VIEWER_TYPE_PANE; else if (result.getViewerType() == RmdEditorOptions.PREVIEW_IN_WINDOW) viewerType = RMD_VIEWER_TYPE_WINDOW; else if (result.getViewerType() == RmdEditorOptions.PREVIEW_IN_NONE) viewerType = RMD_VIEWER_TYPE_NONE; // don't host presentations in the viewer pane--ioslides doesn't scale // slides well without help if (result.isHtmlPresentation() && viewerType == RMD_VIEWER_TYPE_PANE) viewerType = RMD_VIEWER_TYPE_WINDOW; final int newViewerType = viewerType; // if we're about to pop open a window but one of the publish buttons // is waiting for a render to complete, skip the preview entirely so // we don't disturb the publish flow with a window popping up if (newViewerType == RMD_VIEWER_TYPE_WINDOW && RSConnectPublishButton.isAnyRmdRenderPending()) { return; } // get the window object if available WindowEx win = null; boolean needsReopen = false; if (outputFrame_ != null) { win = outputFrame_.getWindowObject(); if (outputFrame_.getViewerType() != newViewerType) needsReopen = true; } // if there's a window up but it's showing a different document type, // close it so that we can create a new one better suited to this doc type if (needsReopen || (win != null && result_ != null && !result_.getFormatName().equals(result.getFormatName()))) { outputFrame_.closeOutputFrame(false); outputFrame_ = null; win = null; // let window finish closing before continuing Scheduler.get().scheduleDeferred(new ScheduledCommand() { @Override public void execute() { displayRenderResult(null, newViewerType, params); } }); } else { displayRenderResult(win, newViewerType, params); } } private void displayRenderResult(WindowEx win, int viewerType, RmdPreviewParams params) { if (viewerType == RMD_VIEWER_TYPE_NONE) return; RmdRenderResult result = params.getResult(); if (outputFrame_ == null) outputFrame_ = createOutputFrame(viewerType); // we're refreshing if the window is up and we're pulling the same // output file as the last one boolean isRefresh = win != null && result_ != null && result_.getOutputFile().equals( result.getOutputFile()); // if this isn't a refresh but there's a window up, cache the scroll // position of the old document before we replace it if (!isRefresh && result_ != null && win != null) { cacheDocPosition(result_, outputFrame_.getScrollPosition(), outputFrame_.getAnchor()); } // if it is a refresh, use the doc's existing positions if (isRefresh) { params.setScrollPosition(outputFrame_.getScrollPosition()); params.setAnchor(outputFrame_.getAnchor()); } boolean isNotebook = result_ != null && FileSystemItem.getExtensionFromPath(result_.getOutputFile()) == NOTEBOOK_EXT; // show the preview; activate the window (but not for auto-refresh of // notebook preview) outputFrame_.showRmdPreview(params, !(isRefresh && isNotebook && result.viewed())); result.setViewed(true); // reset live preview state livePreviewRenderInProgress_ = false; // save the result so we know if the next render is a re-render of the // same document result_ = result; } private final native void exportRmdOutputClosedCallback()/*-{ var registry = this; $wnd.notifyRmdOutputClosed = $entry( function(params) { registry.@org.rstudio.studio.client.rmarkdown.RmdOutput::notifyRmdOutputClosed(Lcom/google/gwt/core/client/JavaScriptObject;)(params); } ); }-*/; // when the window is closed, remember our position within it private void notifyRmdOutputClosed(JavaScriptObject closeParams) { // save anchor location for presentations and scroll position for // documents RmdPreviewParams params = closeParams.cast(); cacheDocPosition(params.getResult(), params.getScrollPosition(), params.getAnchor()); // if this is a Shiny document, stop the associated process if (params.isShinyDocument() && !restarting_) { server_.terminateRenderRmd(true, new VoidServerRequestCallback()); } shinyDoc_ = null; } private void cacheDocPosition(RmdRenderResult result, int scrollPosition, String anchor) { if (result.isHtmlPresentation()) { anchors_.put(keyFromResult(result), anchor); } else { scrollPositions_.put(keyFromResult(result), scrollPosition); } } // Generates lookup keys from results; used to enforce caching scroll // position and/or anchor by document name and type private String keyFromResult(RmdRenderResult result) { if (result.isShinyDocument()) return result.getTargetFile(); else return result.getOutputFile() + "-" + result.getFormatName(); } private RmdOutputFrame createOutputFrame(int viewerType) { switch(viewerType) { case RMD_VIEWER_TYPE_WINDOW: return RStudioGinjector.INSTANCE.getRmdOutputFrameSatellite(); case RMD_VIEWER_TYPE_PANE: return RStudioGinjector.INSTANCE.getRmdOutputFramePane(); } return null; } private final GlobalDisplay globalDisplay_; private final FileTypeRegistry fileTypeRegistry_; private final UIPrefs prefs_; private final PDFViewer pdfViewer_; private final Provider<ViewFilePanel> pViewFilePanel_; private final RMarkdownServerOperations server_; private final Session session_; private final EventBus events_; private final Commands commands_; private final SourceBuildHelper sourceBuildHelper_; private final WorkbenchContext workbenchContext_; private boolean restarting_ = false; // stores the last scroll position of each document we know about: map // of path to position private final Map<String, Integer> scrollPositions_ = new HashMap<String, Integer>(); private final Map<String, String> anchors_ = new HashMap<String, String>(); private RmdRenderResult result_; private RmdShinyDocInfo shinyDoc_; private Operation onRenderCompleted_; private RmdOutputFrame outputFrame_; private boolean renderInProgress_ = false; private boolean livePreviewRenderInProgress_ = false; private boolean quitInitiatedAfterLastRender_ = false; public final static String NOTEBOOK_EXT = ".nb.html"; public final static int TYPE_STATIC = 0; public final static int TYPE_SHINY = 1; public final static int TYPE_NOTEBOOK = 2; public final static int RMD_VIEWER_TYPE_WINDOW = 0; public final static int RMD_VIEWER_TYPE_PANE = 1; public final static int RMD_VIEWER_TYPE_NONE = 2; }