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)