1 /*
2  * SessionRMarkdown.cpp
3  *
4  * Copyright (C) 2021 by RStudio, PBC
5  *
6  * Unless you have received this program directly from RStudio pursuant
7  * to the terms of a commercial license agreement with RStudio, then
8  * this program is licensed to you under the terms of version 3 of the
9  * GNU Affero General Public License. This program is distributed WITHOUT
10  * ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT,
11  * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the
12  * AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details.
13  *
14  */
15 
16 #include "session-config.h"
17 
18 #include "SessionRMarkdown.hpp"
19 
20 #include <gsl/gsl>
21 
22 #include "SessionRmdNotebook.hpp"
23 #include "../SessionHTMLPreview.hpp"
24 #include "../build/SessionBuildErrors.hpp"
25 
26 #include <boost/algorithm/string/predicate.hpp>
27 #include <boost/algorithm/string.hpp>
28 #include <boost/iostreams/filter/regex.hpp>
29 #include <boost/format.hpp>
30 #include <boost/scope_exit.hpp>
31 
32 #include <core/FileSerializer.hpp>
33 #include <core/Exec.hpp>
34 #include <core/system/Environment.hpp>
35 #include <core/system/Process.hpp>
36 #include <core/StringUtils.hpp>
37 #include <core/Algorithm.hpp>
38 #include <core/YamlUtil.hpp>
39 
40 #include <r/RExec.hpp>
41 #include <r/RJson.hpp>
42 #include <r/ROptions.hpp>
43 #include <r/RUtil.hpp>
44 #include <r/RRoutines.hpp>
45 #include <r/RCntxtUtils.hpp>
46 
47 #include <core/r_util/RProjectFile.hpp>
48 
49 #include <session/SessionModuleContext.hpp>
50 #include <session/SessionConsoleProcess.hpp>
51 #include <session/SessionAsyncRProcess.hpp>
52 #include <session/SessionUrlPorts.hpp>
53 #include <session/SessionQuarto.hpp>
54 
55 #include <session/projects/SessionProjects.hpp>
56 #include <session/prefs/UserPrefs.hpp>
57 
58 #include "SessionBlogdown.hpp"
59 #include "RMarkdownPresentation.hpp"
60 
61 #define kRmdOutput "rmd_output"
62 #define kRmdOutputLocation "/" kRmdOutput "/"
63 
64 #define kMathjaxSegment "mathjax"
65 #define kMathjaxBeginComment "<!-- dynamically load mathjax"
66 
67 #define kStandardRenderFunc "rmarkdown::render"
68 #define kShinyRenderFunc "rmarkdown::run"
69 
70 #define kShinyContentWarning "Warning: Shiny application in a static R Markdown document"
71 
72 using namespace rstudio::core;
73 
74 namespace rstudio {
75 namespace session {
76 
77 namespace {
78 
79 bool s_rmarkdownAvailable;
80 bool s_rmarkdownAvailableInited;
81 
82 #ifdef _WIN32
83 
84 // TODO: promote to StringUtils?
utf8ToConsole(const std::string & string)85 std::string utf8ToConsole(const std::string& string)
86 {
87    std::vector<wchar_t> wide(string.length() + 1);
88    int chars = ::MultiByteToWideChar(
89             CP_UTF8, 0,
90             string.data(),
91             gsl::narrow_cast<int>(string.size()),
92             &wide[0],
93             gsl::narrow_cast<int>(wide.size()));
94 
95    if (chars == 0)
96    {
97       LOG_ERROR(LAST_SYSTEM_ERROR());
98       return string;
99    }
100 
101    std::ostringstream output;
102    char buffer[16];
103 
104    // force C locale (ensures that any non-ASCII characters
105    // will fail to convert and hence must be unicode escaped)
106    const char* locale = ::setlocale(LC_CTYPE, nullptr);
107    ::setlocale(LC_CTYPE, "C");
108 
109    for (int i = 0; i < chars; i++)
110    {
111       int n = ::wctomb(buffer, wide[i]);
112 
113       // use Unicode escaping for characters that cannot be represented
114       // as well as for single-byte upper ASCII
115       if (n == -1 || (n == 1 && static_cast<unsigned char>(buffer[0]) > 127))
116       {
117          output << "\\u{" << std::hex << wide[i] << "}";
118       }
119       else
120       {
121          output.write(buffer, n);
122       }
123    }
124 
125    ::setlocale(LC_CTYPE, locale);
126 
127    return output.str();
128 
129 }
130 
131 #else
132 
utf8ToConsole(const std::string & string)133 std::string utf8ToConsole(const std::string& string)
134 {
135    return string_utils::utf8ToSystem(string);
136 }
137 
138 #endif
139 
140 enum
141 {
142    RExecutionReady = 0,
143    RExecutionBusy  = 1
144 };
145 
projectBuildDir()146 std::string projectBuildDir()
147 {
148    return string_utils::utf8ToSystem(
149       projects::projectContext().buildTargetPath().getAbsolutePath());
150 }
151 
detectWebsiteOutputDir(const std::string & siteDir,std::string * pWebsiteOutputDir)152 Error detectWebsiteOutputDir(const std::string& siteDir,
153                              std::string* pWebsiteOutputDir)
154 {
155    r::exec::RFunction websiteOutputDir(".rs.websiteOutputDir", siteDir);
156    return websiteOutputDir.call(pWebsiteOutputDir);
157 }
158 
159 std::string s_websiteOutputDir;
160 
haveMarkdownToHTMLOption()161 bool haveMarkdownToHTMLOption()
162 {
163    SEXP markdownToHTMLOption = r::options::getOption("rstudio.markdownToHTML");
164    return !r::sexp::isNull(markdownToHTMLOption);
165 }
166 
initRmarkdownPackageAvailable()167 void initRmarkdownPackageAvailable()
168 {
169    s_rmarkdownAvailableInited = true;
170    if (!haveMarkdownToHTMLOption())
171    {
172       s_rmarkdownAvailable = r::util::hasRequiredVersion("3.0");
173    }
174    else
175    {
176       s_rmarkdownAvailable = false;
177    }
178 }
179 
initWebsiteOutputDir()180 void initWebsiteOutputDir()
181 {
182    if (!module_context::isWebsiteProject())
183       return;
184 
185    std::string outputDirFullPath;
186    Error error = detectWebsiteOutputDir(projectBuildDir(), &outputDirFullPath);
187    if (error)
188    {
189       LOG_ERROR(error);
190    }
191    else
192    {
193       if (outputDirFullPath != projectBuildDir())
194          s_websiteOutputDir = FilePath(outputDirFullPath).getFilename();
195       else
196          s_websiteOutputDir = "";
197    }
198 }
199 
200 } // anonymous namespace
201 
202 namespace module_context {
203 
extractOutputFileCreated(const FilePath & inputDir,const std::string & output)204 FilePath extractOutputFileCreated(const FilePath& inputDir,
205                                   const std::string& output)
206 {
207    // check each line of the emitted output; if it starts with a token
208    // indicating rendering is complete, store the remainder of the emitted
209    // line as the file we rendered
210    std::vector<std::string> completeMarkers;
211    completeMarkers.push_back("Preview created: ");
212    completeMarkers.push_back("Output created: ");
213    std::string renderLine;
214    std::stringstream outputStream(output);
215    while (std::getline(outputStream, renderLine))
216    {
217       for (const std::string& marker : completeMarkers)
218       {
219          if (boost::algorithm::starts_with(renderLine, marker))
220          {
221             std::string fileName = renderLine.substr(marker.length());
222 
223             // trim any whitespace from the end of the filename (on Windows
224             // this includes part of CR-LF)
225             boost::algorithm::trim(fileName);
226 
227             // if the path looks absolute, use it as-is; otherwise, presume
228             // it to be in the same directory as the input file
229             FilePath outputFile = inputDir.completePath(fileName);
230             if (outputFile.exists())
231                core::system::realPath(outputFile, &outputFile);
232 
233             // if it's a plain .md file and we are in a Hugo project then
234             // don't preview it (as the user is likely running a Hugo preview)
235             if (outputFile.getExtensionLowerCase() == ".md" &&
236                 session::modules::rmarkdown::blogdown::isHugoProject())
237             {
238                return FilePath();
239             }
240 
241             // return the output file
242             return outputFile;
243          }
244       }
245    }
246 
247    return FilePath();
248 }
249 
250 } // namespace module_context
251 
252 namespace modules {
253 namespace rmarkdown {
254 
255 namespace {
256 
257 
258 enum RenderTerminateType
259 {
260    renderTerminateNormal,
261    renderTerminateAbnormal,
262    renderTerminateQuiet
263 };
264 
265 // s_renderOutputs is a rotating buffer that maps an output identifier to a
266 // target location on disk, to give the client access to the last few renders
267 // that occurred in this session without exposing disk paths in the URL. it's
268 // unlikely that the client will ever request a render output other than the
269 // one that was just completed, so keeping > 2 render paths is conservative.
270 #define kMaxRenderOutputs 5
271 std::vector<std::string> s_renderOutputs(kMaxRenderOutputs);
272 int s_currentRenderOutput = 0;
273 
parsableRStudioVersion()274 std::string parsableRStudioVersion()
275 {
276    std::string version(RSTUDIO_VERSION_MAJOR);
277    version.append(".")
278       .append(RSTUDIO_VERSION_MINOR)
279       .append(".")
280       .append(RSTUDIO_VERSION_PATCH)
281       .append(".")
282       .append(boost::regex_replace(
283          std::string(RSTUDIO_VERSION_SUFFIX),
284          boost::regex("[a-zA-Z\\-+]"),
285          ""));
286    return version;
287 }
288 
outputCachePath()289 FilePath outputCachePath()
290 {
291    return module_context::sessionScratchPath().completeChildPath("rmd-outputs");
292 }
293 
assignOutputUrl(const std::string & outputFile)294 std::string assignOutputUrl(const std::string& outputFile)
295 {
296    std::string outputUrl(kRmdOutput "/");
297    s_currentRenderOutput = (s_currentRenderOutput + 1) % kMaxRenderOutputs;
298 
299    // if this is a website project and the file is not at the root then we need
300    // to do some special handling to make sure that the HTML can refer to
301    // locations in parent directories (e.g. for navigation links)
302    std::string path = "/";
303    FilePath outputPath = module_context::resolveAliasedPath(outputFile);
304    FilePath websiteDir = r_util::websiteRootDirectory(outputPath);
305    if (!websiteDir.isEmpty())
306    {
307       std::string websiteOutputDir;
308       Error error = detectWebsiteOutputDir(websiteDir.getAbsolutePath(), &websiteOutputDir);
309       if (error)
310       {
311          websiteDir = FilePath();
312          LOG_ERROR(error);
313       }
314       else
315       {
316          websiteDir = FilePath(websiteOutputDir);
317       }
318    }
319 
320    // figure out the project directory
321    FilePath projDir = outputPath.getParent();
322    if (projDir.getFilename() == "_site")
323       projDir = projDir.getParent();
324 
325    // detect whether we're creating a book output vs. a website page
326    if (!websiteDir.isEmpty() && outputPath.isWithin(websiteDir) && !r_util::isWebsiteDirectory(projDir))
327    {
328       std::string renderedPath;
329       Error error = r::exec::RFunction(".rs.bookdown.renderedOutputPath")
330             .addUtf8Param(websiteDir)
331             .addUtf8Param(outputPath)
332             .callUtf8(&renderedPath);
333       if (error)
334          LOG_ERROR(error);
335 
336       s_renderOutputs[s_currentRenderOutput] = renderedPath;
337 
338       // compute relative path to target file and append it to the path
339       std::string relativePath = outputPath.getRelativePath(websiteDir);
340       path += relativePath;
341    }
342    else
343    {
344       s_renderOutputs[s_currentRenderOutput] = outputFile;
345    }
346 
347    outputUrl.append(boost::lexical_cast<std::string>(s_currentRenderOutput));
348    outputUrl.append(path);
349    return outputUrl;
350 }
351 
getOutputFormat(const std::string & path,const std::string & encoding,json::Object * pResultJson)352 void getOutputFormat(const std::string& path,
353                      const std::string& encoding,
354                      json::Object* pResultJson)
355 {
356    // query rmarkdown for the output format
357    json::Object& resultJson = *pResultJson;
358    r::sexp::Protect protect;
359    SEXP sexpOutputFormat;
360    Error error = r::exec::RFunction("rmarkdown:::default_output_format",
361                                     string_utils::utf8ToSystem(path), encoding)
362                                    .call(&sexpOutputFormat, &protect);
363    if (error)
364    {
365       resultJson["format_name"] = "";
366       resultJson["self_contained"] = false;
367    }
368    else
369    {
370       std::string formatName;
371       error = r::sexp::getNamedListElement(sexpOutputFormat, "name",
372                                            &formatName);
373       if (error)
374          LOG_ERROR(error);
375       resultJson["format_name"] = formatName;
376 
377       SEXP sexpOptions;
378       bool selfContained = false;
379       error = r::sexp::getNamedListSEXP(sexpOutputFormat, "options",
380                                         &sexpOptions);
381       if (error)
382          LOG_ERROR(error);
383       else
384       {
385          error = r::sexp::getNamedListElement(sexpOptions, "self_contained",
386                                               &selfContained, false);
387          if (error)
388             LOG_ERROR(error);
389       }
390 
391       resultJson["self_contained"] = selfContained;
392    }
393 }
394 
395 
396 class RenderRmd : public async_r::AsyncRProcess
397 {
398 public:
create(const FilePath & targetFile,int sourceLine,const std::string & format,const std::string & encoding,const std::string & paramsFile,bool sourceNavigation,bool asTempfile,bool asShiny,const std::string & existingOutputFile,const std::string & workingDir,const std::string & viewerType)399    static boost::shared_ptr<RenderRmd> create(const FilePath& targetFile,
400                                               int sourceLine,
401                                               const std::string& format,
402                                               const std::string& encoding,
403                                               const std::string& paramsFile,
404                                               bool sourceNavigation,
405                                               bool asTempfile,
406                                               bool asShiny,
407                                               const std::string& existingOutputFile,
408                                               const std::string& workingDir,
409                                               const std::string& viewerType)
410    {
411       boost::shared_ptr<RenderRmd> pRender(new RenderRmd(targetFile,
412                                                          sourceLine,
413                                                          sourceNavigation,
414                                                          asShiny));
415       pRender->start(format, encoding, paramsFile, asTempfile,
416                      existingOutputFile, workingDir, viewerType);
417       return pRender;
418    }
419 
terminateProcess(RenderTerminateType terminateType)420    void terminateProcess(RenderTerminateType terminateType)
421    {
422       terminateType_ = terminateType;
423       async_r::AsyncRProcess::terminate();
424    }
425 
outputFile()426    FilePath outputFile()
427    {
428       return outputFile_;
429    }
430 
getPresentationDetails(int sourceLine,json::Object * jsonObject)431    void getPresentationDetails(int sourceLine, json::Object* jsonObject)
432    {
433       // default to no slide info
434       (*jsonObject)["preview_slide"] = -1;
435       (*jsonObject)["slide_navigation"] = json::Value();
436 
437       // only allow extended results if we have a source file to
438       // navigate back to (otherwise you could navigate to the temp
439       // file used for preview)
440       if (sourceNavigation_)
441       {
442          rmarkdown::presentation::ammendResults(
443                   outputFormat_["format_name"].getString(),
444                   targetFile_,
445                   sourceLine,
446                   jsonObject);
447       }
448    }
449 
getRuntime(const FilePath & targetFile)450    std::string getRuntime(const FilePath& targetFile)
451    {
452       std::string runtime;
453       Error error = r::exec::RFunction(
454          ".rs.getRmdRuntime",
455          string_utils::utf8ToSystem(targetFile.getAbsolutePath())).call(
456                                                                &runtime);
457       if (error)
458          LOG_ERROR(error);
459       return runtime;
460    }
461 
462 private:
RenderRmd(const FilePath & targetFile,int sourceLine,bool sourceNavigation,bool asShiny)463    RenderRmd(const FilePath& targetFile, int sourceLine, bool sourceNavigation,
464              bool asShiny) :
465       terminateType_(renderTerminateAbnormal),
466       isShiny_(asShiny),
467       hasShinyContent_(false),
468       targetFile_(targetFile),
469       sourceLine_(sourceLine),
470       sourceNavigation_(sourceNavigation)
471    {
472    }
473 
start(const std::string & format,const std::string & encoding,const std::string & paramsFile,bool asTempfile,const std::string & existingOutputFile,const std::string & workingDir,const std::string & viewerType)474    void start(const std::string& format,
475               const std::string& encoding,
476               const std::string& paramsFile,
477               bool asTempfile,
478               const std::string& existingOutputFile,
479               const std::string& workingDir,
480               const std::string& viewerType)
481    {
482       Error error;
483       json::Object dataJson;
484       getOutputFormat(targetFile_.getAbsolutePath(), encoding, &outputFormat_);
485       dataJson["output_format"] = outputFormat_;
486       dataJson["target_file"] = module_context::createAliasedPath(targetFile_);
487       ClientEvent event(client_events::kRmdRenderStarted, dataJson);
488       module_context::enqueClientEvent(event);
489 
490       // save encoding and viewer type
491       encoding_ = encoding;
492       viewerType_ = viewerType;
493 
494       std::string renderFunc;
495       if (isShiny_)
496       {
497          std::string extendedType;
498          Error error = source_database::detectExtendedType(targetFile_, &extendedType);
499          if (error)
500             LOG_ERROR(error);
501          if (extendedType == "quarto-document")
502             renderFunc = "quarto run";
503          else
504             renderFunc = kShinyRenderFunc;
505       }
506       else
507       {
508          // see if the input file has a custom render function
509          error = r::exec::RFunction(
510             ".rs.getCustomRenderFunction",
511             string_utils::utf8ToSystem(targetFile_.getAbsolutePath())).call(
512                                                                   &renderFunc);
513          if (error)
514             LOG_ERROR(error);
515 
516          if (renderFunc.empty())
517             renderFunc = kStandardRenderFunc;
518          else if (renderFunc == kShinyRenderFunc)
519             isShiny_ = true;
520       }
521 
522       // if we are using a quarto command to render, we must be a quarto doc. read
523       // all of the input file lines to be used in error navigation
524       if (renderFunc == "quarto run" || renderFunc == "quarto render")
525       {
526           isQuarto_ = true;
527           Error error = core::readLinesFromFile(targetFile_, &targetFileLines_);
528           if (error)
529              LOG_ERROR(error);
530       }
531 
532       std::string extraParams;
533       std::string targetFile =
534             utf8ToConsole(targetFile_.getAbsolutePath());
535 
536       std::string renderOptions("encoding = '" + encoding + "'");
537 
538       // output to a specific format if specified
539       if (!format.empty())
540       {
541          renderOptions += ", output_format = '" + format + "'";
542       }
543 
544       // include params if specified
545       if (!paramsFile.empty())
546       {
547          renderOptions += ", params = readRDS('" + utf8ToConsole(paramsFile) + "')";
548       }
549 
550       // use the stated working directory if specified and we're using the default render function
551       // (other render functions may not accept knit_root_dir)
552       if (!workingDir.empty() && renderFunc == kStandardRenderFunc)
553       {
554          renderOptions += ", knit_root_dir = '" +
555                           utf8ToConsole(workingDir) + "'";
556       }
557 
558       // output to a temporary directory if specified (no need to do this
559       // for Shiny since it already renders to a temporary dir)
560       if (asTempfile && !isShiny_)
561       {
562          FilePath tmpDir = module_context::tempFile("preview-", "dir");
563          Error error = tmpDir.ensureDirectory();
564          if (!error)
565          {
566             std::string dir = utf8ToConsole(tmpDir.getAbsolutePath());
567             renderOptions += ", output_dir = '" + dir + "'";
568          }
569          else
570          {
571             LOG_ERROR(error);
572          }
573       }
574 
575       if (isShiny_)
576       {
577          extraParams += "shiny_args = list(launch.browser = FALSE), "
578                         "auto_reload = FALSE, ";
579          std::string parentDir = utf8ToConsole(targetFile_.getParent().getAbsolutePath());
580          extraParams += "dir = '" + parentDir + "', ";
581 
582          // provide render_args in render_args parameter
583          renderOptions = "render_args = list(" + renderOptions + ")";
584       }
585 
586       // fallback for custom render function that isn't actually a function
587       if (renderFunc != kStandardRenderFunc && renderFunc != kShinyRenderFunc)
588       {
589          r::sexp::Protect rProtect;
590          SEXP renderFuncSEXP;
591          error = r::exec::evaluateString(renderFunc, &renderFuncSEXP, &rProtect);
592          if (error || !r::sexp::isFunction((renderFuncSEXP)))
593          {
594             boost::format fmt("(function(input, ...) { invisible(system(paste0('%1% \"', input, '\"'))) })");
595             renderFunc = boost::str(fmt % renderFunc);
596          }
597       }
598 
599       // render command
600       boost::format fmt("%1%('%2%', %3% %4%);");
601       std::string cmd = boost::str(fmt %
602                              renderFunc %
603                              string_utils::singleQuotedStrEscape(targetFile) %
604                              extraParams %
605                              renderOptions);
606 
607       // un-escape unicode escapes
608 #ifdef _WIN32
609       cmd = boost::algorithm::replace_all_copy(cmd, "\\\\u{", "\\u{");
610 #endif
611 
612       // environment
613       core::system::Options environment;
614       std::string tempDir;
615       error = r::exec::RFunction("tempdir").call(&tempDir);
616       if (!error)
617          environment.push_back(std::make_pair("RMARKDOWN_PREVIEW_DIR", tempDir));
618       else
619          LOG_ERROR(error);
620 
621       // pass along the RSTUDIO_VERSION
622       environment.push_back(std::make_pair("RSTUDIO_VERSION", parsableRStudioVersion()));
623       environment.push_back(std::make_pair("RSTUDIO_LONG_VERSION", RSTUDIO_VERSION));
624 
625       // set the not cran env var
626       environment.push_back(std::make_pair("NOT_CRAN", "true"));
627 
628       // pass along the current Python environment, if any
629       std::string reticulatePython;
630       error = r::exec::RFunction(".rs.inferReticulatePython").call(&reticulatePython);
631       if (error)
632          LOG_ERROR(error);
633 
634       // pass along current PATH
635       std::string currentPath = core::system::getenv("PATH");
636       core::system::setenv(&environment, "PATH", currentPath);
637 
638       if (!reticulatePython.empty())
639       {
640          // we found a Python version; forward it
641          environment.push_back({"RETICULATE_PYTHON", reticulatePython});
642 
643          // also update the PATH so this version of Python is visible
644          core::system::addToPath(
645                   &environment,
646                   FilePath(reticulatePython).getParent().getAbsolutePath(),
647                   true);
648       }
649 
650       // render unless we were handed an existing output file
651       allOutput_.clear();
652       if (existingOutputFile.empty())
653       {
654          // launch the R session in the document's directory by default, unless
655          // a working directory was supplied
656          FilePath working = targetFile_.getParent();
657          if (!workingDir.empty())
658             working = module_context::resolveAliasedPath(workingDir);
659 
660          // tell the user the command we're using to render the doc if requested
661          if (prefs::userPrefs().showRmdRenderCommand())
662          {
663             onRenderOutput(module_context::kCompileOutputNormal, "==> " + cmd + "\n");
664          }
665 
666          // start the render process
667          async_r::AsyncRProcess::start(cmd.c_str(), environment, working,
668                                        async_r::R_PROCESS_NO_RDATA);
669       }
670       else
671       {
672          // if we are handed an existing output file then this is the build
673          // tab previewing a website. in this case the build tab opened
674          // for the build and forced the viewer pane to a smaller height. as
675          // a result we want to do a forceMaximize to restore the Viewer
676          // pane. Note that these two concerns happen to conflate here but
677          // it's conceivable that there would be other forceMaximize
678          // scenarios or that other types of previews where an output file
679          // was already in hand would NOT want to do a forceMaximize. We're
680          // leaving this coupling for now to minimze the scope of the change
681          // required to allow website previews to restore the viewer pane, we
682          // may want a more intrusive change if/when we discover other
683          // scenarios.
684          outputFile_ = module_context::resolveAliasedPath(existingOutputFile);
685          terminate(true, true);
686       }
687    }
688 
onStdout(const std::string & output)689    void onStdout(const std::string& output)
690    {
691       onRenderOutput(module_context::kCompileOutputNormal,
692                      string_utils::systemToUtf8(output));
693    }
694 
onStderr(const std::string & output)695    void onStderr(const std::string& output)
696    {
697       onRenderOutput(module_context::kCompileOutputError,
698                      string_utils::systemToUtf8(output));
699    }
700 
onRenderOutput(int type,const std::string & output)701    void onRenderOutput(int type, const std::string& output)
702    {
703       // buffer output
704       allOutput_.append(output);
705 
706       enqueRenderOutput(type, output);
707 
708       std::vector<std::string> outputLines;
709       boost::algorithm::split(outputLines, output,
710                               boost::algorithm::is_any_of("\n\r"));
711       for (std::string& outputLine : outputLines)
712       {
713          // if this is a Shiny render, check to see if Shiny started listening
714          if (isShiny_)
715          {
716             const boost::regex shinyListening("^Listening on (http.*)$");
717             boost::smatch matches;
718             if (regex_utils::match(outputLine, matches, shinyListening))
719             {
720                json::Object startedJson;
721                startedJson["target_file"] =
722                      module_context::createAliasedPath(targetFile_);
723                startedJson["output_format"] = outputFormat_;
724                std::string url(url_ports::mapUrlPorts(matches[1].str()));
725 
726                // add a / to the URL if it doesn't have one already
727                // (typically portmapped URLs do, but the raw URL returned by
728                // Shiny doesn't)
729                if (url[url.length() - 1] != '/')
730                   url += "/";
731 
732                getPresentationDetails(sourceLine_, &startedJson);
733 
734                startedJson["url"] = url + targetFile_.getFilename();
735 
736                startedJson["runtime"] = getRuntime(targetFile_);
737 
738                startedJson["is_quarto"] = isQuarto_;
739 
740                module_context::enqueClientEvent(ClientEvent(
741                            client_events::kRmdShinyDocStarted,
742                            startedJson));
743                break;
744             }
745          }
746 
747          // check to see if a warning was emitted indicating that this document
748          // contains Shiny content
749          if (outputLine.substr(0, sizeof(kShinyContentWarning)) ==
750              kShinyContentWarning)
751          {
752             hasShinyContent_ = true;
753          }
754       }
755    }
756 
onCompleted(int exitStatus)757    void onCompleted(int exitStatus)
758    {
759       // see if we can determine the output file
760       FilePath outputFile = module_context::extractOutputFileCreated
761                                                    (targetFile_.getParent(), allOutput_);
762       if (!outputFile.isEmpty())
763       {
764          // record ouptut file
765          outputFile_ = outputFile;
766 
767          // see if the quarto module wants to handle the preview
768          if (quarto::handleQuartoPreview(targetFile_, outputFile_, allOutput_, true))
769             viewerType_ = kRmdViewerTypeNone;
770       }
771 
772 
773       // the process may be terminated normally by the IDE (e.g. to stop the
774       // Shiny server); alternately, a termination is considered normal if
775       // the process succeeded and produced output.
776       terminate(terminateType_ == renderTerminateNormal ||
777                 (exitStatus == 0 && outputFile_.exists()));
778    }
779 
terminateWithError(const Error & error)780    void terminateWithError(const Error& error)
781    {
782       std::string message =
783             "Error rendering R Markdown for " +
784             module_context::createAliasedPath(targetFile_) + " " +
785             error.getSummary();
786       terminateWithError(message);
787    }
788 
terminateWithError(const std::string & message)789    void terminateWithError(const std::string& message)
790    {
791       enqueRenderOutput(module_context::kCompileOutputError, message);
792       terminate(false);
793    }
794 
terminate(bool succeeded)795    void terminate(bool succeeded)
796    {
797       terminate(succeeded, false);
798    }
799 
terminate(bool succeeded,bool forceMaximize)800    void terminate(bool succeeded, bool forceMaximize)
801    {
802       using namespace module_context;
803 
804       markCompleted();
805 
806       // if a quiet terminate was requested, don't queue any client events
807       if (terminateType_ == renderTerminateQuiet)
808          return;
809 
810       json::Object resultJson;
811       resultJson["succeeded"] = succeeded;
812       resultJson["target_file"] = createAliasedPath(targetFile_);
813       resultJson["target_encoding"] = encoding_;
814       resultJson["target_line"] = sourceLine_;
815 
816       std::string outputFile = createAliasedPath(outputFile_);
817       resultJson["output_file"] = outputFile;
818 
819       std::vector<SourceMarker> knitrErrors;
820       if (renderErrorMarker_)
821       {
822          renderErrorMarker_.message = core::html_utils::HTML(renderErrorMessage_.str());
823          knitrErrors.push_back(renderErrorMarker_);
824       }
825       resultJson["knitr_errors"] = sourceMarkersAsJson(knitrErrors);
826 
827       resultJson["output_url"] = assignOutputUrl(outputFile);
828       resultJson["output_format"] = outputFormat_;
829 
830       resultJson["is_shiny_document"] = isShiny_;
831       resultJson["has_shiny_content"] = hasShinyContent_;
832       resultJson["is_quarto"] = isQuarto_;
833 
834       resultJson["runtime"] = getRuntime(targetFile_);
835 
836       json::Value websiteDir;
837       if (outputFile_.getExtensionLowerCase() == ".html")
838       {
839          // check for previous publishing
840          resultJson["rpubs_published"] =
841                !module_context::previousRpubsUploadId(outputFile_).empty();
842 
843          FilePath webPath = session::projects::projectContext().fileUnderWebsitePath(targetFile_);
844          if (!webPath.isEmpty())
845             websiteDir = createAliasedPath(webPath);
846       }
847       else
848       {
849          resultJson["rpubs_published"] = false;
850       }
851 
852       resultJson["website_dir"] = websiteDir;
853 
854       // view options
855       resultJson["force_maximize"] = forceMaximize;
856       resultJson["viewer_type"] = viewerType_;
857 
858       // allow for format specific additions to the result json
859       std::string formatName =  outputFormat_["format_name"].getString();
860 
861       // populate slide information if available
862       getPresentationDetails(sourceLine_, &resultJson);
863 
864       // if we failed then we may want to enque additional diagnostics
865       if (!succeeded)
866          enqueFailureDiagnostics(formatName);
867 
868       ClientEvent event(client_events::kRmdRenderCompleted, resultJson);
869       module_context::enqueClientEvent(event);
870    }
871 
enqueFailureDiagnostics(const std::string & formatName)872    void enqueFailureDiagnostics(const std::string& formatName)
873    {
874       if ((formatName == "pdf_document" ||
875            formatName == "beamer_presentation")
876           && !module_context::isPdfLatexInstalled())
877       {
878          enqueRenderOutput(module_context::kCompileOutputError,
879             "\nNo LaTeX installation detected (LaTeX is required "
880             "to create PDF output). You should install "
881             "a LaTeX distribution for your platform: "
882             "https://www.latex-project.org/get/\n\n"
883             "  If you are not sure, you may install TinyTeX in R: tinytex::install_tinytex()\n\n"
884             "  Otherwise consider MiKTeX on Windows - http://miktex.org\n\n"
885             "  MacTeX on macOS - https://tug.org/mactex/\n"
886             "  (NOTE: Download with Safari rather than Chrome _strongly_ recommended)\n\n"
887             "  Linux: Use system package manager\n\n");
888       }
889    }
890 
enqueRenderOutput(int type,const std::string & output)891    void enqueRenderOutput(int type,
892                           const std::string& output)
893    {
894       using namespace module_context;
895 
896       if (type == kCompileOutputError && sourceNavigation_)
897       {
898          if (renderErrorMarker_)
899          {
900             // if an error occurred during rendering, then any
901             // subsequent output should be gathered as part of
902             // that error
903             renderErrorMessage_ << output;
904          }
905          else if (isQuarto_)
906          {
907             // check for a jupyter error if this is quarto
908             int errLine = module_context::jupyterErrorLineNumber(targetFileLines_, allOutput_);
909             if (errLine != -1)
910             {
911                module_context::editFile(targetFile_, errLine);
912             }
913          }
914          else
915          {
916             // check whether an error occurred while rendering the document and
917             // if knitr is about to exit. look for a specific quit marker and
918             // parse to to gather information for a source marker
919             const char* renderErrorPattern =
920                   "(?:.*?)Quitting from lines (\\d+)-(\\d+) \\(([^)]+)\\)(.*)";
921 
922             boost::regex reRenderError(renderErrorPattern);
923             boost::smatch matches;
924             if (regex_utils::match(output, matches, reRenderError))
925             {
926                // looks like a knitr error; compose a compile error object and
927                // emit it to the client when the render is complete
928                int line = core::safe_convert::stringTo<int>(matches[1].str(), -1);
929                FilePath file = targetFile_.getParent().completePath(matches[3].str());
930                renderErrorMarker_ = SourceMarker(SourceMarker::Error, file, line, 1, {}, true);
931                renderErrorMessage_ << matches[4].str();
932             }
933          }
934       }
935 
936       // always enque quarto as normal output (it does it's own colorizing of error output)
937       if (isQuarto_)
938          type = module_context::kCompileOutputNormal;
939 
940       CompileOutput compileOutput(type, output);
941       ClientEvent event(
942                client_events::kRmdRenderOutput,
943                compileOutputAsJson(compileOutput));
944       module_context::enqueClientEvent(event);
945    }
946 
947    RenderTerminateType terminateType_;
948    bool isShiny_;
949    bool hasShinyContent_;
950    bool isQuarto_ = false;
951    FilePath targetFile_;
952    std::vector<std::string> targetFileLines_;
953    int sourceLine_;
954    FilePath outputFile_;
955    std::string encoding_;
956    std::string viewerType_;
957    bool sourceNavigation_;
958    json::Object outputFormat_;
959    module_context::SourceMarker renderErrorMarker_;
960    std::stringstream renderErrorMessage_;
961    std::string allOutput_;
962 };
963 
964 boost::shared_ptr<RenderRmd> s_pCurrentRender_;
965 
966 // replaces references to MathJax with references to our built-in resource
967 // handler.
968 // in:  script src = "http://foo/bar/Mathjax.js?abc=123"
969 // out: script src = "mathjax/MathJax.js?abc=123"
970 //
971 // if no MathJax use is found in the document, removes the script src statement
972 // entirely, so we don't incur the cost of loading MathJax in preview mode
973 // unless the document actually has markup.
974 class MathjaxFilter : public boost::iostreams::regex_filter
975 {
976 public:
MathjaxFilter()977    MathjaxFilter()
978       // the regular expression matches any of the three tokens that look
979       // like the beginning of math, and the "script src" line itself
980       : boost::iostreams::regex_filter(
981             boost::regex(kMathjaxBeginComment "|"
982                          "\\\\\\[|\\\\\\(|<math|"
983                          "^(\\s*script.src\\s*=\\s*)\"http.*?(MathJax.js[^\"]*)\""),
984             boost::bind(&MathjaxFilter::substitute, this, _1)),
985         hasMathjax_(false)
986    {
987    }
988 
989 private:
substitute(const boost::cmatch & match)990    std::string substitute(const boost::cmatch& match)
991    {
992       std::string result;
993 
994       if (match[0] == "\\[" ||
995           match[0] == "\\(" ||
996           match[0] == "<math")
997       {
998          // if we found one of the MathJax markup start tokens, we need to emit
999          // MathJax scripts
1000          hasMathjax_ = true;
1001          return match[0];
1002       }
1003       else if (match[0] == kMathjaxBeginComment)
1004       {
1005          // we found the start of the MathJax section; add the MathJax config
1006          // block if we're in a configuration that requires it
1007 #if defined(_WIN32)
1008          if (session::options().programMode() != kSessionProgramModeDesktop)
1009             return match[0];
1010 
1011          result.append(kQtMathJaxConfigScript "\n");
1012          result.append(match[0]);
1013 #else
1014          return match[0];
1015 #endif
1016       }
1017       else if (hasMathjax_)
1018       {
1019          // this is the MathJax script itself; emit it if we found a start token
1020          result.append(match[1]);
1021          result.append("\"" kMathjaxSegment "/");
1022          result.append(match[2]);
1023          result.append("\"");
1024       }
1025 
1026       return result;
1027    }
1028 
1029    bool hasMathjax_;
1030 };
1031 
isRenderRunning()1032 bool isRenderRunning()
1033 {
1034    return s_pCurrentRender_ && s_pCurrentRender_->isRunning();
1035 }
1036 
1037 
1038 // environment variables to initialize
1039 const char * const kRStudioPandoc = "RSTUDIO_PANDOC";
1040 const char * const kRmarkdownMathjaxPath = "RMARKDOWN_MATHJAX_PATH";
1041 
initEnvironment()1042 void initEnvironment()
1043 {
1044    // set RSTUDIO_PANDOC (leave existing value alone)
1045    std::string rstudioPandoc = core::system::getenv(kRStudioPandoc);
1046    if (rstudioPandoc.empty())
1047       rstudioPandoc = session::options().pandocPath().getAbsolutePath();
1048    r::exec::RFunction sysSetenv("Sys.setenv");
1049    sysSetenv.addParam(kRStudioPandoc, rstudioPandoc);
1050 
1051    // set RMARKDOWN_MATHJAX_PATH (leave existing value alone)
1052    std::string rmarkdownMathjaxPath = core::system::getenv(kRmarkdownMathjaxPath);
1053    if (rmarkdownMathjaxPath.empty())
1054      rmarkdownMathjaxPath = session::options().mathjaxPath().getAbsolutePath();
1055    sysSetenv.addParam(kRmarkdownMathjaxPath, rmarkdownMathjaxPath);
1056 
1057    // call Sys.setenv
1058    Error error = sysSetenv.call();
1059    if (error)
1060       LOG_ERROR(error);
1061 }
1062 
1063 
1064 // when the RMarkdown package is installed, give .Rmd files the extended type
1065 // "rmarkdown", unless there is a marker that indicates we should
1066 // use the previous rendering strategy
onDetectRmdSourceType(boost::shared_ptr<source_database::SourceDocument> pDoc)1067 std::string onDetectRmdSourceType(
1068       boost::shared_ptr<source_database::SourceDocument> pDoc)
1069 {
1070    if (!pDoc->path().empty())
1071    {
1072       FilePath filePath = module_context::resolveAliasedPath(pDoc->path());
1073       if ((filePath.getExtensionLowerCase() == ".rmd" ||
1074            filePath.getExtensionLowerCase() == ".md") &&
1075           !boost::algorithm::icontains(pDoc->contents(),
1076                                        "<!-- rmarkdown v1 -->") &&
1077           rmarkdownPackageAvailable())
1078       {
1079          // if we find html_notebook in the YAML header, presume that this is an R Markdown notebook
1080          // (this isn't 100% foolproof but this check runs frequently so needs to be fast; more
1081          // thorough type introspection is done on the client)
1082          std::string yamlHeader = yaml::extractYamlHeader(pDoc->contents());
1083          if (boost::algorithm::contains(yamlHeader, "html_notebook"))
1084          {
1085             return "rmarkdown-notebook";
1086          }
1087 
1088          // otherwise, it's a regular R Markdown document
1089          return "rmarkdown-document";
1090       }
1091    }
1092    return std::string();
1093 }
1094 
onClientInit()1095 void onClientInit()
1096 {
1097    // if a new client is connecting, shut any running render process
1098    // (these processes can have virtually unbounded lifetime because they
1099    // leave a server running in the Shiny document case)
1100    if (s_pCurrentRender_ && s_pCurrentRender_->isRunning())
1101       s_pCurrentRender_->terminateProcess(renderTerminateQuiet);
1102 }
1103 
getRMarkdownContext(const json::JsonRpcRequest &,json::JsonRpcResponse * pResponse)1104 Error getRMarkdownContext(const json::JsonRpcRequest&,
1105                           json::JsonRpcResponse* pResponse)
1106 {
1107    json::Object contextJson;
1108    pResponse->setResult(contextJson);
1109    return Success();
1110 }
1111 
doRenderRmd(const std::string & file,int line,const std::string & format,const std::string & encoding,const std::string & paramsFile,bool sourceNavigation,bool asTempfile,bool asShiny,const std::string & existingOutputFile,const std::string & workingDir,const std::string & viewerType,json::JsonRpcResponse * pResponse)1112 void doRenderRmd(const std::string& file,
1113                  int line,
1114                  const std::string& format,
1115                  const std::string& encoding,
1116                  const std::string& paramsFile,
1117                  bool sourceNavigation,
1118                  bool asTempfile,
1119                  bool asShiny,
1120                  const std::string& existingOutputFile,
1121                  const std::string& workingDir,
1122                  const std::string& viewerType,
1123                  json::JsonRpcResponse* pResponse)
1124 {
1125    if (s_pCurrentRender_ &&
1126        s_pCurrentRender_->isRunning())
1127    {
1128       pResponse->setResult(false);
1129    }
1130    else
1131    {
1132       s_pCurrentRender_ = RenderRmd::create(
1133                module_context::resolveAliasedPath(file),
1134                line,
1135                format,
1136                encoding,
1137                paramsFile,
1138                sourceNavigation,
1139                asTempfile,
1140                asShiny,
1141                existingOutputFile,
1142                workingDir,
1143                viewerType);
1144       pResponse->setResult(true);
1145    }
1146 }
1147 
renderRmd(const json::JsonRpcRequest & request,json::JsonRpcResponse * pResponse)1148 Error renderRmd(const json::JsonRpcRequest& request,
1149                 json::JsonRpcResponse* pResponse)
1150 {
1151    int line = -1, type = kRenderTypeStatic;
1152    std::string file, format, encoding, paramsFile, existingOutputFile,
1153                workingDir, viewerType;
1154    bool asTempfile = false;
1155    Error error = json::readParams(request.params,
1156                                   &file,
1157                                   &line,
1158                                   &format,
1159                                   &encoding,
1160                                   &paramsFile,
1161                                   &asTempfile,
1162                                   &type,
1163                                   &existingOutputFile,
1164                                   &workingDir,
1165                                   &viewerType);
1166    if (error)
1167       return error;
1168 
1169    if (type == kRenderTypeNotebook)
1170    {
1171       // if this is a notebook, it's pre-rendered
1172       FilePath inputFile = module_context::resolveAliasedPath(file);
1173       FilePath outputFile = inputFile.getParent().completePath(inputFile.getStem() +
1174                                                         kNotebookExt);
1175 
1176       // extract the output format
1177       json::Object outputFormat;
1178 
1179       // TODO: this should use getOutputFormat(), but we can't read format
1180       // defaults yet since the html_notebook type doesn't exist in the
1181       // rmarkdown package yet
1182       outputFormat["format_name"] = "html_notebook";
1183       outputFormat["self_contained"] = true;
1184 
1185       json::Object resultJson;
1186       resultJson["succeeded"] = outputFile.exists();
1187       resultJson["target_file"] = file;
1188       resultJson["target_encoding"] = encoding;
1189       resultJson["target_line"] = line;
1190       resultJson["output_file"] = module_context::createAliasedPath(outputFile);
1191       resultJson["knitr_errors"] = json::Array();
1192       resultJson["output_url"] = assignOutputUrl(outputFile.getAbsolutePath());
1193       resultJson["output_format"] = outputFormat;
1194       resultJson["is_shiny_document"] = false;
1195       resultJson["website_dir"] = json::Value();
1196       resultJson["has_shiny_content"] = false;
1197       resultJson["rpubs_published"] =
1198             !module_context::previousRpubsUploadId(outputFile).empty();
1199       resultJson["force_maximize"] = false;
1200       resultJson["viewer_type"] = viewerType;
1201       ClientEvent event(client_events::kRmdRenderCompleted, resultJson);
1202       module_context::enqueClientEvent(event);
1203    }
1204    else
1205    {
1206       // not a notebook, do render work
1207       doRenderRmd(file, line, format, encoding, paramsFile,
1208                   true, asTempfile, type == kRenderTypeShiny, existingOutputFile,
1209                   workingDir, viewerType, pResponse);
1210    }
1211 
1212    return Success();
1213 }
1214 
renderRmdSource(const json::JsonRpcRequest & request,json::JsonRpcResponse * pResponse)1215 Error renderRmdSource(const json::JsonRpcRequest& request,
1216                      json::JsonRpcResponse* pResponse)
1217 {
1218    std::string source;
1219    Error error = json::readParams(request.params, &source);
1220    if (error)
1221       return error;
1222 
1223    // create temp file
1224    FilePath rmdTempFile = module_context::tempFile("Preview-", "Rmd");
1225    error = core::writeStringToFile(rmdTempFile, source);
1226    if (error)
1227       return error;
1228 
1229    doRenderRmd(
1230       rmdTempFile.getAbsolutePath(), -1, "", "UTF-8", "",
1231                false, false, false, "", "", "", pResponse);
1232 
1233    return Success();
1234 }
1235 
1236 
terminateRenderRmd(const json::JsonRpcRequest & request,json::JsonRpcResponse *)1237 Error terminateRenderRmd(const json::JsonRpcRequest& request,
1238                          json::JsonRpcResponse*)
1239 {
1240    bool normal;
1241    Error error = json::readParams(request.params, &normal);
1242    if (error)
1243       return error;
1244 
1245    if (isRenderRunning())
1246    {
1247       module_context::clearViewerCurrentUrl();
1248 
1249       s_pCurrentRender_->terminateProcess(
1250                normal ? renderTerminateNormal :
1251                         renderTerminateAbnormal);
1252 
1253    }
1254 
1255    return Success();
1256 }
1257 
1258 // return the path to the local copy of MathJax
mathJaxDirectory()1259 FilePath mathJaxDirectory()
1260 {
1261    // prefer the environment variable if it exists
1262    std::string mathjaxPath = core::system::getenv(kRmarkdownMathjaxPath);
1263    if (!mathjaxPath.empty() && FilePath::exists(mathjaxPath))
1264       return FilePath(mathjaxPath);
1265    else
1266       return session::options().mathjaxPath();
1267 }
1268 
1269 // Handles a request for RMarkdown output. This request embeds the name of
1270 // the file to be viewed as an encoded part of the URL. For instance, requests
1271 // to show render output for ~/abc.html and its resources look like:
1272 //
1273 // http://<server>/rmd_output/~%252Fabc.html/...
1274 //
1275 // Note that this requires two URL encoding passes at the origin, since a
1276 // a URL decoding pass is made on the whole URL before this handler is invoked.
handleRmdOutputRequest(const http::Request & request,http::Response * pResponse)1277 void handleRmdOutputRequest(const http::Request& request,
1278                             http::Response* pResponse)
1279 {
1280    std::string path = http::util::pathAfterPrefix(request,
1281                                                   kRmdOutputLocation);
1282 
1283    // find the portion of the URL containing the output identifier
1284    size_t pos = path.find('/', 1);
1285    if (pos == std::string::npos)
1286    {
1287       pResponse->setNotFoundError(request);
1288       return;
1289    }
1290 
1291    // extract the output identifier
1292    int outputId = 0;
1293    try
1294    {
1295       outputId = boost::lexical_cast<int>(path.substr(0, pos));
1296    }
1297    catch (boost::bad_lexical_cast const&)
1298    {
1299       pResponse->setNotFoundError(request);
1300       return;
1301    }
1302 
1303    // make sure the output identifier refers to a valid file
1304    std::string outputFile = s_renderOutputs[outputId];
1305    FilePath outputFilePath(module_context::resolveAliasedPath(outputFile));
1306    if (!outputFilePath.exists())
1307    {
1308       pResponse->setNotFoundError(outputFile, request);
1309       return;
1310    }
1311 
1312    // strip the output identifier from the URL
1313    path = path.substr(pos + 1, path.length());
1314 
1315    if (path.empty())
1316    {
1317       // disable caching; the request path looks identical to the browser for
1318       // each main request for content
1319       pResponse->setNoCacheHeaders();
1320 
1321       // serve the contents of the file with MathJax URLs mapped to our
1322       // own resource handler
1323       MathjaxFilter mathjaxFilter;
1324       pResponse->setFile(outputFilePath, request, mathjaxFilter);
1325 
1326       // set the content-type to ensure UTF-8 (all pandoc output
1327       // is UTF-8 encoded)
1328       pResponse->setContentType("text/html; charset=utf-8");
1329    }
1330    else if (boost::algorithm::starts_with(path, kMathjaxSegment))
1331    {
1332       // serve the MathJax resource: find the requested path in the MathJax
1333       // directory
1334       pResponse->setCacheableFile(
1335          mathJaxDirectory().completePath(
1336             path.substr(sizeof(kMathjaxSegment))),
1337                                   request);
1338    }
1339    else
1340    {
1341       // serve a file resource from the output folder
1342       FilePath filePath = outputFilePath.getParent().completeChildPath(path);
1343 
1344       // if it's a directory then auto-append index.html
1345       if (filePath.isDirectory())
1346          filePath = filePath.completeChildPath("index.html");
1347 
1348       html_preview::addFileSpecificHeaders(filePath, pResponse);
1349       pResponse->setNoCacheHeaders();
1350       pResponse->setFile(filePath, request);
1351    }
1352 }
1353 
1354 
createRmdFromTemplate(const json::JsonRpcRequest & request,json::JsonRpcResponse * pResponse)1355 Error createRmdFromTemplate(const json::JsonRpcRequest& request,
1356                             json::JsonRpcResponse* pResponse)
1357 {
1358    std::string filePath, templatePath, resultPath;
1359    bool createDir;
1360    Error error = json::readParams(request.params,
1361                                   &filePath,
1362                                   &templatePath,
1363                                   &createDir);
1364    if (error)
1365       return error;
1366 
1367    r::exec::RFunction draft("rmarkdown:::draft");
1368    draft.addParam("file", filePath);
1369    draft.addParam("template", templatePath);
1370    draft.addParam("create_dir", createDir);
1371    draft.addParam("edit", false);
1372    error = draft.call(&resultPath);
1373 
1374    if (error)
1375       return error;
1376 
1377    json::Object jsonResult;
1378    jsonResult["path"] = resultPath;
1379    pResponse->setResult(jsonResult);
1380 
1381    return Success();
1382 }
1383 
getRmdTemplate(const json::JsonRpcRequest & request,json::JsonRpcResponse * pResponse)1384 Error getRmdTemplate(const json::JsonRpcRequest& request,
1385                      json::JsonRpcResponse* pResponse)
1386 {
1387    std::string path;
1388    Error error = json::readParams(request.params, &path);
1389    if (error)
1390       return error;
1391 
1392    json::Object jsonResult;
1393 
1394    // locate the template skeleton on disk
1395    // (return empty string if none found)
1396    std::string templateContent;
1397    for (auto&& suffix : { "skeleton/skeleton.Rmd", "skeleton/skeleton.rmd" })
1398    {
1399       FilePath skeletonPath = FilePath(path).completePath(suffix);
1400       if (!skeletonPath.exists())
1401          continue;
1402 
1403       Error error = readStringFromFile(skeletonPath, &templateContent, string_utils::LineEndingPosix);
1404       if (error)
1405       {
1406          LOG_ERROR(error);
1407          continue;
1408       }
1409 
1410       break;
1411    }
1412 
1413    jsonResult["content"] = templateContent;
1414    pResponse->setResult(jsonResult);
1415    return Success();
1416 }
1417 
prepareForRmdChunkExecution(const json::JsonRpcRequest & request,json::JsonRpcResponse * pResponse)1418 Error prepareForRmdChunkExecution(const json::JsonRpcRequest& request,
1419                                   json::JsonRpcResponse* pResponse)
1420 {
1421    // read id param
1422    std::string id;
1423    Error error = json::readParams(request.params, &id);
1424    if (error)
1425       return error;
1426 
1427    error = evaluateRmdParams(id);
1428    if (error)
1429    {
1430       LOG_ERROR(error);
1431       return error;
1432    }
1433 
1434    // indicate to the client whether R currently has executing code on the
1435    // stack
1436    json::Object result;
1437    result["state"] = r::context::globalContext().nextcontext() ?
1438       RExecutionReady : RExecutionBusy;
1439    pResponse->setResult(result);
1440 
1441    return Success();
1442 }
1443 
1444 
maybeCopyWebsiteAsset(const json::JsonRpcRequest & request,json::JsonRpcResponse * pResponse)1445 Error maybeCopyWebsiteAsset(const json::JsonRpcRequest& request,
1446                        json::JsonRpcResponse* pResponse)
1447 {
1448    std::string file;
1449    Error error = json::readParams(request.params, &file);
1450    if (error)
1451       return error;
1452 
1453    // don't copy if we build inline
1454    std::string websiteOutputDir = module_context::websiteOutputDir();
1455    if (websiteOutputDir.empty())
1456    {
1457       pResponse->setResult(true);
1458       return Success();
1459    }
1460 
1461    // get the path relative to the website dir
1462    FilePath websiteDir = projects::projectContext().buildTargetPath();
1463    FilePath filePath = module_context::resolveAliasedPath(file);
1464    std::string relativePath = filePath.getRelativePath(websiteDir);
1465 
1466    // get the list of copyable site resources
1467    std::vector<std::string> copyableResources;
1468    r::exec::RFunction func("rmarkdown:::copyable_site_resources");
1469    func.addParam("input", string_utils::utf8ToSystem(websiteDir.getAbsolutePath()));
1470    func.addParam("encoding", projects::projectContext().config().encoding);
1471    error = func.call(&copyableResources);
1472    if (error)
1473    {
1474       LOG_ERROR(error);
1475       pResponse->setResult(false);
1476       return Success();
1477    }
1478 
1479    // get the name to target -- if it's in the root dir it's the filename
1480    // otherwise it's the directory name
1481    std::string search;
1482    if (filePath.getParent() == websiteDir)
1483       search = filePath.getFilename();
1484    else
1485       search = relativePath.substr(0, relativePath.find_first_of('/'));
1486 
1487    // if it's not in the list we don't copy it
1488    if (!algorithm::contains(copyableResources, search))
1489    {
1490        pResponse->setResult(false);
1491        return Success();
1492    }
1493 
1494    // copy the file (removing it first)
1495    FilePath outputDir = FilePath(websiteOutputDir);
1496    FilePath outputFile = outputDir.completeChildPath(relativePath);
1497    if (outputFile.exists())
1498    {
1499       error = outputFile.remove();
1500       if (error)
1501       {
1502          LOG_ERROR(error);
1503          pResponse->setResult(false);
1504          return Success();
1505       }
1506    }
1507 
1508    error = outputFile.getParent().ensureDirectory();
1509    if (error)
1510    {
1511       LOG_ERROR(error);
1512       pResponse->setResult(false);
1513       return Success();
1514    }
1515 
1516    error = filePath.copy(outputFile);
1517    if (error)
1518    {
1519       LOG_ERROR(error);
1520       pResponse->setResult(false);
1521    }
1522    else
1523    {
1524       pResponse->setResult(true);
1525    }
1526 
1527    return Success();
1528 }
1529 
rmdImportImages(const json::JsonRpcRequest & request,json::JsonRpcResponse * pResponse)1530 Error rmdImportImages(const json::JsonRpcRequest& request,
1531                       json::JsonRpcResponse* pResponse)
1532 {
1533    // read params
1534    json::Array imagesJson;
1535    std::string imagesDir;
1536    Error error = json::readParams(request.params, &imagesJson, &imagesDir);
1537    if (error)
1538       return error;
1539 
1540    // determine images dir
1541    FilePath imagesPath = module_context::resolveAliasedPath(imagesDir);
1542    error = imagesPath.ensureDirectory();
1543    if (error)
1544       return error;
1545 
1546    // build list of target image paths
1547    json::Array importedImagesJson;
1548 
1549    // copy each image to the target directory (renaming with a unique stem as required)
1550    std::vector<std::string> images;
1551    imagesJson.toVectorString(images);
1552    for (auto image : images)
1553    {
1554       // skip if it doesn't exist
1555       FilePath imagePath = module_context::resolveAliasedPath(image);
1556       if (!imagePath.exists())
1557          continue;
1558 
1559       // find a unique target path
1560       std::string targetStem = imagePath.getStem();
1561       std::string extension = imagePath.getExtension();
1562       FilePath targetPath = imagesPath.completeChildPath(targetStem + extension);
1563       if (imagesPath.completeChildPath(targetStem + extension).exists())
1564       {
1565          std::string resolvedStem;
1566          module_context::uniqueSaveStem(imagesPath, targetStem, "-", &resolvedStem);
1567          targetPath = imagesPath.completeChildPath(resolvedStem + extension);
1568       }
1569 
1570       // only copy it if it's not the same path
1571       if (imagePath != targetPath)
1572       {
1573          Error error = imagePath.copy(targetPath);
1574          if (error)
1575             LOG_ERROR(error);
1576       }
1577 
1578       // update imported images
1579       importedImagesJson.push_back(module_context::createAliasedPath(targetPath));
1580    }
1581 
1582    pResponse->setResult(importedImagesJson);
1583    return Success();
1584 }
1585 
rs_paramsFileForRmd(SEXP fileSEXP)1586 SEXP rs_paramsFileForRmd(SEXP fileSEXP)
1587 {
1588    static std::map<std::string,std::string> s_paramsFiles;
1589 
1590    std::string file = r::sexp::safeAsString(fileSEXP);
1591 
1592    using namespace module_context;
1593    if (s_paramsFiles.find(file) == s_paramsFiles.end())
1594       s_paramsFiles[file] = createAliasedPath(tempFile("rmdparams", "rds"));
1595 
1596    r::sexp::Protect rProtect;
1597    return r::sexp::create(s_paramsFiles[file], &rProtect);
1598 }
1599 
rs_getWebsiteOutputDir()1600 SEXP rs_getWebsiteOutputDir()
1601 {
1602    SEXP absolutePathSEXP = R_NilValue;
1603 
1604    FilePath outputDir(module_context::websiteOutputDir());
1605    if (!outputDir.isEmpty())
1606    {
1607       r::sexp::Protect protect;
1608       absolutePathSEXP = r::sexp::create(outputDir.getAbsolutePath(), &protect);
1609    }
1610    return absolutePathSEXP;
1611 }
1612 
onShutdown(bool terminatedNormally)1613 void onShutdown(bool terminatedNormally)
1614 {
1615    Error error = core::writeStringVectorToFile(outputCachePath(),
1616                                                s_renderOutputs);
1617    if (error)
1618       LOG_ERROR(error);
1619 }
1620 
onSuspend(const r::session::RSuspendOptions &,core::Settings *)1621 void onSuspend(const r::session::RSuspendOptions&, core::Settings*)
1622 {
1623    onShutdown(true);
1624 }
1625 
onResume(const Settings &)1626 void onResume(const Settings&)
1627 {
1628 }
1629 
1630 } // anonymous namespace
1631 
evaluateRmdParams(const std::string & docId)1632 Error evaluateRmdParams(const std::string& docId)
1633 {
1634    // get document contents
1635    using namespace source_database;
1636    boost::shared_ptr<SourceDocument> pDoc(new SourceDocument());
1637    Error error = source_database::get(docId, pDoc);
1638    if (error)
1639       return error;
1640 
1641    // evaluate params if we can
1642    if (module_context::isPackageVersionInstalled("knitr", "1.10"))
1643    {
1644       error = r::exec::RFunction(".rs.evaluateRmdParams", pDoc->contents())
1645                                 .call();
1646       if (error)
1647          return error;
1648    }
1649    return Success();
1650 }
1651 
knitParamsAvailable()1652 bool knitParamsAvailable()
1653 {
1654    return module_context::isPackageVersionInstalled("rmarkdown", "0.7.3") &&
1655           module_context::isPackageVersionInstalled("knitr", "1.10.18");
1656 }
1657 
knitWorkingDirAvailable()1658 bool knitWorkingDirAvailable()
1659 {
1660    return module_context::isPackageVersionInstalled("rmarkdown", "1.1.9017");
1661 }
1662 
pptAvailable()1663 bool pptAvailable()
1664 {
1665    return module_context::isPackageVersionInstalled("rmarkdown", "1.8.10");
1666 }
1667 
rmarkdownPackageAvailable()1668 bool rmarkdownPackageAvailable()
1669 {
1670    if (!s_rmarkdownAvailableInited)
1671    {
1672       if (!r::exec::isMainThread())
1673       {
1674          LOG_WARNING_MESSAGE(" Accessing rmarkdownPackageAvailable() from thread other than main");
1675          return false;
1676       }
1677       initRmarkdownPackageAvailable();
1678    }
1679 
1680    return s_rmarkdownAvailable;
1681 }
1682 
isSiteProject(const std::string & site)1683 bool isSiteProject(const std::string& site)
1684 {
1685    if (!modules::rmarkdown::rmarkdownPackageAvailable() ||
1686        !projects::projectContext().hasProject() ||
1687        projects::projectContext().config().buildType != r_util::kBuildTypeWebsite)
1688       return false;
1689 
1690    bool isSite = false;
1691    std::string encoding = projects::projectContext().defaultEncoding();
1692    Error error = r::exec::RFunction(".rs.isSiteProject",
1693                                     projectBuildDir(), encoding, site).call(&isSite);
1694    if (error)
1695       LOG_ERROR(error);
1696    return isSite;
1697 }
1698 
1699 
initialize()1700 Error initialize()
1701 {
1702    using boost::bind;
1703    using namespace module_context;
1704 
1705    RS_REGISTER_CALL_METHOD(rs_paramsFileForRmd);
1706    RS_REGISTER_CALL_METHOD(rs_getWebsiteOutputDir);
1707 
1708    initEnvironment();
1709 
1710    module_context::events().onDeferredInit.connect(
1711                                  boost::bind(initWebsiteOutputDir));
1712    module_context::events().onDetectSourceExtendedType
1713                                         .connect(onDetectRmdSourceType);
1714    module_context::events().onClientInit.connect(onClientInit);
1715    module_context::events().onShutdown.connect(onShutdown);
1716    module_context::addSuspendHandler(SuspendHandler(onSuspend, onResume));
1717 
1718    // load output paths if saved
1719    FilePath cachePath = outputCachePath();
1720    if (cachePath.exists())
1721    {
1722       Error error = core::readStringVectorFromFile(cachePath, &s_renderOutputs);
1723       if (error)
1724          LOG_ERROR(error);
1725       else
1726       {
1727          s_currentRenderOutput = gsl::narrow_cast<int>(s_renderOutputs.size());
1728          s_renderOutputs.reserve(kMaxRenderOutputs);
1729       }
1730    }
1731 
1732    ExecBlock initBlock;
1733    initBlock.addFunctions()
1734       (bind(registerRpcMethod, "get_rmarkdown_context", getRMarkdownContext))
1735       (bind(registerRpcMethod, "render_rmd", renderRmd))
1736       (bind(registerRpcMethod, "render_rmd_source", renderRmdSource))
1737       (bind(registerRpcMethod, "terminate_render_rmd", terminateRenderRmd))
1738       (bind(registerRpcMethod, "create_rmd_from_template", createRmdFromTemplate))
1739       (bind(registerRpcMethod, "get_rmd_template", getRmdTemplate))
1740       (bind(registerRpcMethod, "prepare_for_rmd_chunk_execution", prepareForRmdChunkExecution))
1741       (bind(registerRpcMethod, "maybe_copy_website_asset", maybeCopyWebsiteAsset))
1742       (bind(registerRpcMethod, "rmd_import_images", rmdImportImages))
1743       (bind(registerUriHandler, kRmdOutputLocation, handleRmdOutputRequest))
1744       (bind(module_context::sourceModuleRFile, "SessionRMarkdown.R"));
1745 
1746    return initBlock.execute();
1747 }
1748 
1749 } // namespace rmarkdown
1750 } // namespace modules
1751 
1752 namespace module_context {
1753 
1754 
isWebsiteProject()1755 bool isWebsiteProject()
1756 {
1757    if (!modules::rmarkdown::rmarkdownPackageAvailable())
1758       return false;
1759 
1760    return (projects::projectContext().config().buildType ==
1761            r_util::kBuildTypeWebsite);
1762 }
1763 
1764 // used to determine if this is both a website build target AND a bookdown target
isBookdownWebsite()1765 bool isBookdownWebsite()
1766 {
1767    return isWebsiteProject() && isBookdownProject();
1768 }
1769 
1770 // used to determine whether the current project directory has a bookdown project
1771 // (distinct from isBookdownWebsite b/c includes scenarios where the book is
1772 // built by a makefile rather than "Build Website"
isBookdownProject()1773 bool isBookdownProject()
1774 {
1775    if (!projects::projectContext().hasProject())
1776       return false;
1777 
1778    bool isBookdown = false;
1779    std::string encoding = projects::projectContext().defaultEncoding();
1780    Error error = r::exec::RFunction(".rs.isBookdownDir",
1781                               projectBuildDir(), encoding).call(&isBookdown);
1782    if (error)
1783       LOG_ERROR(error);
1784    return isBookdown;
1785 }
1786 
isDistillProject()1787 bool isDistillProject()
1788 {
1789    if (!isWebsiteProject())
1790       return false;
1791 
1792    return session::modules::rmarkdown::isSiteProject("distill_website");
1793 }
1794 
1795 
websiteOutputDir()1796 std::string websiteOutputDir()
1797 {
1798    return s_websiteOutputDir;
1799 }
1800 
1801 } // namespace module_context
1802 
1803 } // namespace session
1804 } // namespace rstudio
1805 
1806