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 ¶msFile,
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(©ableResources);
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