diff --git a/app/controllers/annotations_controller.rb b/app/controllers/annotations_controller.rb
index 33ffe88c4b..9679770b6b 100644
--- a/app/controllers/annotations_controller.rb
+++ b/app/controllers/annotations_controller.rb
@@ -34,7 +34,7 @@ def add_existing_annotation
page: params[:page],
**base_attributes
)
- elsif submission_file.is_pynb?
+ elsif submission_file.is_pynb? || submission_file.is_rmd?
@annotation = result.annotations.create!(
type: 'HtmlAnnotation',
start_node: params[:start_node],
@@ -107,7 +107,7 @@ def create
page: params[:page],
**base_attributes
)
- elsif submission_file.is_pynb?
+ elsif submission_file.is_pynb? || submission_file.is_rmd?
@annotation = result.annotations.create!(
type: 'HtmlAnnotation',
start_node: params[:start_node],
diff --git a/app/controllers/submissions_controller.rb b/app/controllers/submissions_controller.rb
index 39e56e2842..6e76950f70 100644
--- a/app/controllers/submissions_controller.rb
+++ b/app/controllers/submissions_controller.rb
@@ -18,7 +18,9 @@ class SubmissionsController < ApplicationController
p.frame_src(*PERMITTED_IFRAME_SRC)
end
- content_security_policy_report_only only: :html_content
+ content_security_policy only: :html_content do |p|
+ p.style_src :self, "'unsafe-inline'"
+ end
def index
respond_to do |format|
@@ -55,9 +57,9 @@ def repo_browser
end
# store the displayed revision
if @revision.nil? && ((params[:revision_identifier] &&
- params[:revision_identifier] == revision.revision_identifier.to_s) ||
- (params[:revision_timestamp] &&
- Time.zone.parse(params[:revision_timestamp]).in_time_zone >= revision.server_timestamp))
+ params[:revision_identifier] == revision.revision_identifier.to_s) ||
+ (params[:revision_timestamp] &&
+ Time.zone.parse(params[:revision_timestamp]).in_time_zone >= revision.server_timestamp))
@revision = revision
end
end
@@ -482,7 +484,8 @@ def download_file
return head :not_found
end
- if params[:show_in_browser] == 'true' && file.is_pynb? && Rails.application.config.nbconvert_enabled
+ if params[:show_in_browser] == 'true' &&
+ ((file.is_pynb? && Rails.application.config.nbconvert_enabled) || file.is_rmd?)
redirect_to html_content_course_assignment_submissions_url(current_course,
record.grouping.assignment,
select_file_id: params[:select_file_id])
@@ -559,8 +562,9 @@ def download_file_zip
def download
preview = params[:preview] == 'true'
nbconvert_enabled = Rails.application.config.nbconvert_enabled
+ file_type = FileHelper.get_file_type(params[:file_name])
- if FileHelper.get_file_type(params[:file_name]) == 'jupyter-notebook' && preview && nbconvert_enabled
+ if ((file_type == 'jupyter-notebook' && nbconvert_enabled) || file_type == 'markdown') && preview
redirect_to action: :html_content,
course_id: current_course.id,
assignment_id: params[:assignment_id],
@@ -678,7 +682,11 @@ def html_content
else
sanitized_filename = ActiveStorage::Filename.new("#{filename}.#{revision_identifier}").sanitized
unique_path = File.join(grouping.group.repo_name, path, sanitized_filename)
- @html_content = notebook_to_html(file_contents, unique_path, @file_type)
+ if @file_type == 'markdown'
+ @html_content = rmd_to_html(file_contents, unique_path)
+ else
+ @html_content = notebook_to_html(file_contents, unique_path, @notebook_type)
+ end
end
render layout: 'html_content'
end
@@ -945,6 +953,43 @@ def notebook_to_html(file_contents, unique_path, type)
File.read(cache_file)
end
+ def rmd_to_html(file_contents, unique_path)
+ cache_file = Pathname.new('tmp/rmd_html_cache') + "#{unique_path}.html"
+ unless File.exist? cache_file
+ FileUtils.mkdir_p(cache_file.dirname)
+ begin
+ temp_rmd_file = Tempfile.new(['temp_rmd_content', '.Rmd'], 'tmp/rmd_html_cache')
+ file_contents.gsub!(/```{r[^\}]*?(echo|eval|include)\s*=\s*(TRUE|FALSE)[^\}]*?}/, '```{r}')
+ temp_rmd_file.write(file_contents)
+ temp_rmd_file.close
+
+ args = [
+ 'Rscript', '-e',
+ "library(rmarkdown); library(knitr); knitr::opts_chunk$set(eval = FALSE, echo = TRUE, include = TRUE); \
+ rmarkdown::render('#{temp_rmd_file.path}', output_format = 'html_document', \
+ output_file = '#{Rails.root.join(cache_file)}')"
+ ]
+
+ _stdout, stderr, status = Open3.capture3(*args)
+ return "#{I18n.t('submissions.cannot_display')}
#{stderr.lines.last}" unless status.exitstatus.zero?
+ rescue StandardError => e
+ return "#{I18n.t('submissions.invalid_rmd_content')}: #{e}"
+ ensure
+ temp_rmd_file&.unlink
+ end
+
+ html = Nokogiri::HTML.parse(File.read(cache_file))
+ current_ids = html.xpath('//*[@id]').pluck(:id).to_set # rubocop:disable Rails/PluckId
+ html.xpath('//*[not(@id)]').map do |elem|
+ unique_id = elem.path
+ unique_id += '-next' while current_ids.include? unique_id
+ elem.set_attribute(:id, unique_id)
+ end
+ File.write(cache_file, html.to_html)
+ end
+ File.read(cache_file)
+ end
+
# Return a relative path to a temporary zip file (which may or may not exists).
# The name of this file is unique by the +assignment+ and current user.
def zipped_grouping_file_name(assignment)