diff --git a/app/services/reports/docx.rb b/app/services/reports/docx.rb index 4fd93088f..75686b989 100644 --- a/app/services/reports/docx.rb +++ b/app/services/reports/docx.rb @@ -34,53 +34,5 @@ class Reports::Docx end @docx end - - def self.link_prepare(scinote_url, link) - link[0] == '/' ? scinote_url + link : link - end - - def self.render_p_element(docx, element, options = {}) - scinote_url = options[:scinote_url] - link_style = options[:link_style] - docx.p do - element[:children].each do |text_el| - if text_el[:type] == 'text' - style = text_el[:style] || {} - text text_el[:value], style - text ' ' if text_el[:value] != '' - elsif text_el[:type] == 'br' && !options[:skip_br] - br - elsif text_el[:type] == 'a' - if text_el[:link] - link_url = Reports::Docx.link_prepare(scinote_url, text_el[:link]) - link text_el[:value], link_url, link_style - else - text text_el[:value], link_style - end - text ' ' if text_el[:value] != '' - end - end - end - end - - def self.render_img_element(docx, element, options = {}) - style = element[:style] - - if options[:table] - max_width = (style[:max_width] / options[:table][:columns].to_f) - if style[:width] > max_width - style[:height] = (max_width / style[:width].to_f) * style[:height] - style[:width] = max_width - end - end - - docx.img element[:data] do - data element[:blob].download - width style[:width] - height style[:height] - align style[:align] || :left - end - end end - # rubocop:enable Style/ClassAndModuleChildren diff --git a/app/services/reports/docx/draw_experiment.rb b/app/services/reports/docx/draw_experiment.rb index e61595846..b1aa74350 100644 --- a/app/services/reports/docx/draw_experiment.rb +++ b/app/services/reports/docx/draw_experiment.rb @@ -22,7 +22,8 @@ module Reports::Docx::DrawExperiment link_style end html = custom_auto_link(experiment.description, team: @report_team) - html_to_word_converter(html) + Reports::HtmlToWordConverter.new(@docx, { scinote_url: scinote_url, + link_style: link_style }).html_to_word_converter(html) @docx.p subject['children'].each do |child| public_send("draw_#{child['type_of']}", child, experiment) diff --git a/app/services/reports/docx/draw_my_module.rb b/app/services/reports/docx/draw_my_module.rb index 1f38b7435..c18b2896b 100644 --- a/app/services/reports/docx/draw_my_module.rb +++ b/app/services/reports/docx/draw_my_module.rb @@ -66,7 +66,8 @@ module Reports::Docx::DrawMyModule if my_module.description.present? html = custom_auto_link(my_module.description, team: @report_team) - html_to_word_converter(html) + Reports::HtmlToWordConverter.new(@docx, { scinote_url: scinote_url, + link_style: link_style }).html_to_word_converter(html) else @docx.p I18n.t('projects.reports.elements.module.no_description') end diff --git a/app/services/reports/docx/draw_my_module_activity.rb b/app/services/reports/docx/draw_my_module_activity.rb index fcf753a68..8747e5937 100644 --- a/app/services/reports/docx/draw_my_module_activity.rb +++ b/app/services/reports/docx/draw_my_module_activity.rb @@ -20,7 +20,7 @@ module Reports::Docx::DrawMyModuleActivity sanitize_input(generate_activity_content(activity, true)) end @docx.p I18n.l(activity_ts, format: :full), color: color[:gray] - html_to_word_converter(activity_text) + Reports::HtmlToWordConverter.new(@docx).html_to_word_converter(activity_text) @docx.p end end diff --git a/app/services/reports/docx/draw_my_module_protocol.rb b/app/services/reports/docx/draw_my_module_protocol.rb index 97d04fc07..881c79aa1 100644 --- a/app/services/reports/docx/draw_my_module_protocol.rb +++ b/app/services/reports/docx/draw_my_module_protocol.rb @@ -11,7 +11,7 @@ module Reports::Docx::DrawMyModuleProtocol timestamp: I18n.l(protocol.created_at, format: :full) @docx.hr html = custom_auto_link(protocol.description, team: @report_team) - html_to_word_converter(html) + Reports::HtmlToWordConverter.new(@docx).html_to_word_converter(html) @docx.p @docx.p end diff --git a/app/services/reports/docx/draw_result_asset.rb b/app/services/reports/docx/draw_result_asset.rb index b1fda37fa..fb5ffa88d 100644 --- a/app/services/reports/docx/draw_result_asset.rb +++ b/app/services/reports/docx/draw_result_asset.rb @@ -17,7 +17,7 @@ module Reports::Docx::DrawResultAsset user: result.user.full_name, timestamp: I18n.l(timestamp, format: :full)), color: color[:gray] end - asset_image_preparing(asset) if asset.image? + Reports::DocxRenderer.render_asset_image(@docx, asset) if asset.image? subject['children'].each do |child| public_send("draw_#{child['type_of']}", child, result) diff --git a/app/services/reports/docx/draw_result_comments.rb b/app/services/reports/docx/draw_result_comments.rb index b738808d8..101290b24 100644 --- a/app/services/reports/docx/draw_result_comments.rb +++ b/app/services/reports/docx/draw_result_comments.rb @@ -17,7 +17,8 @@ module Reports::Docx::DrawResultComments date: I18n.l(comment_ts, format: :full_date), time: I18n.l(comment_ts, format: :time)), italic: true html = custom_auto_link(comment.message, team: @report_team) - html_to_word_converter(html) + Reports::HtmlToWordConverter.new(@docx, { scinote_url: @scinote_url, + link_style: @link_style }).html_to_word_converter(html) @docx.p end end diff --git a/app/services/reports/docx/draw_result_text.rb b/app/services/reports/docx/draw_result_text.rb index ee87953e0..33abb9867 100644 --- a/app/services/reports/docx/draw_result_text.rb +++ b/app/services/reports/docx/draw_result_text.rb @@ -17,7 +17,8 @@ module Reports::Docx::DrawResultText timestamp: I18n.l(timestamp, format: :full), user: result.user.full_name), color: color[:gray] end html = custom_auto_link(result_text.text, team: @report_team) - html_to_word_converter(html) + Reports::HtmlToWordConverter.new(@docx, { scinote_url: @scinote_url, + link_style: @link_style }).html_to_word_converter(html) subject['children'].each do |child| public_send("draw_#{child['type_of']}", child, result) diff --git a/app/services/reports/docx/draw_step.rb b/app/services/reports/docx/draw_step.rb index 064d3a85a..87ac65c7a 100644 --- a/app/services/reports/docx/draw_step.rb +++ b/app/services/reports/docx/draw_step.rb @@ -27,7 +27,8 @@ module Reports::Docx::DrawStep end if step.description.present? html = custom_auto_link(step.description, team: @report_team) - html_to_word_converter(html) + Reports::HtmlToWordConverter.new(@docx, { scinote_url: @scinote_url, + link_style: @link_style }).html_to_word_converter(html) else @docx.p I18n.t 'projects.reports.elements.step.no_description' end diff --git a/app/services/reports/docx/draw_step_asset.rb b/app/services/reports/docx/draw_step_asset.rb index e8560ddf0..80a87c8da 100644 --- a/app/services/reports/docx/draw_step_asset.rb +++ b/app/services/reports/docx/draw_step_asset.rb @@ -15,6 +15,6 @@ module Reports::Docx::DrawStepAsset timestamp: I18n.l(timestamp, format: :full)), color: color[:gray] end - asset_image_preparing(asset) if asset.image? + Reports::DocxRenderer.render_asset_image(@docx, asset) if asset.image? end end diff --git a/app/services/reports/docx/draw_step_comments.rb b/app/services/reports/docx/draw_step_comments.rb index 5e04956ec..40bee154d 100644 --- a/app/services/reports/docx/draw_step_comments.rb +++ b/app/services/reports/docx/draw_step_comments.rb @@ -17,7 +17,8 @@ module Reports::Docx::DrawStepComments date: I18n.l(comment_ts, format: :full_date), time: I18n.l(comment_ts, format: :time)), italic: true html = custom_auto_link(comment.message, team: @report_team) - html_to_word_converter(html) + Reports::HtmlToWordConverter.new(@docx, { scinote_url: @scinote_url, + link_style: @link_style }).html_to_word_converter(html) @docx.p end end diff --git a/app/services/reports/docx/private_methods.rb b/app/services/reports/docx/private_methods.rb index 257d71e4b..3a17a5d77 100644 --- a/app/services/reports/docx/private_methods.rb +++ b/app/services/reports/docx/private_methods.rb @@ -3,222 +3,6 @@ module Reports::Docx::PrivateMethods private - # RTE fields support - def html_to_word_converter(text) - html = Nokogiri::HTML(text) - raw_elements = recursive_children(html.css('body').children, []) - - # Combined raw text blocks in paragraphs - elements = combine_docx_elements(raw_elements) - - # Draw elements - elements.each do |elem| - if elem[:type] == 'p' - Reports::Docx.render_p_element(@docx, elem, scinote_url: @scinote_url, link_style: @link_style) - elsif elem[:type] == 'table' - tiny_mce_table(elem[:data]) - elsif elem[:type] == 'newline' - style = elem[:style] || {} - # print heading if its heading - # Mixing heading with other style setting causes problems for Word - if %w(h1 h2 h3 h4 h5).include?(style[:style]) - @docx.public_send(style[:style], elem[:value]) - else - @docx.p elem[:value] do - align style[:align] - color style[:color] - bold style[:bold] - italic style[:italic] - end - end - elsif elem[:type] == 'image' - Reports::Docx.render_img_element(@docx, elem) - end - end - end - - def combine_docx_elements(raw_elements) - elements = [] - temp_p = [] - raw_elements.each do |elem| - if %w(image newline table).include? elem[:type] - unless temp_p.empty? - elements.push(type: 'p', children: temp_p) - temp_p = [] - end - elements.push(elem) - elsif %w(br text a).include? elem[:type] - temp_p.push(elem) - end - end - elements.push(type: 'p', children: temp_p) - elements - end - - # Convert HTML structure to plain text structure - def recursive_children(children, elements, options = {}) - children.each do |elem| - if elem.class == Nokogiri::XML::Text - next if elem.text.strip == ' ' # Invisible symbol - - style = paragraph_styling(elem.parent) - type = (style[:align] && style[:align] != :justify) || style[:style] ? 'newline' : 'text' - - text = smart_annotation_check(elem) - - elements.push( - type: type, - value: text.strip.delete(' '), # Invisible symbol - style: style - ) - next - end - - if elem.name == 'br' - elements.push(type: 'br') - next - end - - if elem.name == 'img' && elem.attributes['data-mce-token'] - - image = TinyMceAsset.find_by(id: Base62.decode(elem.attributes['data-mce-token'].value)) - next unless image - - image_path = image_path(image.image) - dimension = FastImage.size(image_path) - - next unless dimension - - style = image_styling(elem, dimension) - - elements.push( - type: 'image', - data: image_path.split('&')[0], - blob: image.blob, - style: style - ) - next - end - - if elem.name == 'a' - elements.push(link_element(elem)) - next - end - - if elem.name == 'table' - elem = tiny_mce_table(elem, nested_table: true) if options[:nested_tables] - elements.push( - type: 'table', - data: elem - ) - next - end - - elements = recursive_children(elem.children, elements) if elem.children - end - elements - end - - def link_element(elem) - text = elem.text - link = elem.attributes['href'].value if elem.attributes['href'] - if elem.attributes['class']&.value == 'record-info-link' - link = nil - text = "##{text}" - end - text = "##{text}" if elem.parent.attributes['class']&.value == 'atwho-inserted' - text = "@#{text}" if elem.attributes['class']&.value == 'atwho-user-popover' - { - type: 'a', - value: text, - link: link - } - end - - def smart_annotation_check(elem) - return "[#{elem.text}]" if elem.parent.attributes['class']&.value == 'sa-type' - - elem.text - end - - # Prepare style for text - def paragraph_styling(elem) - style = elem.attributes['style'] - result = {} - result[:style] = elem.name if elem.name.include? 'h' - result[:bold] = true if elem.name == 'strong' - result[:italic] = true if elem.name == 'em' - style_keys = %w(text-align color) - - if style - style_keys.each do |key| - style_el = style.value.split(';').select { |i| (i.include? key) }[0] - next unless style_el - - value = style_el.split(':')[1].strip if style_el - if key == 'text-align' - result[:align] = value.to_sym - elsif key == 'color' && calculate_color_hsp(value) < 190 - result[:color] = value.delete('#') - end - end - end - result - end - - # Prepare style for images - def image_styling(elem, dimension) - dimension[0] = elem.attributes['width'].value.to_i if elem.attributes['width'] - dimension[1] = elem.attributes['height'].value.to_i if elem.attributes['height'] - - if elem.attributes['style'] - align = if elem.attributes['style'].value.include? 'margin-right' - :center - elsif elem.attributes['style'].value.include? 'float: right' - :right - else - :left - end - end - - margins = Constants::REPORT_DOCX_MARGIN_LEFT + Constants::REPORT_DOCX_MARGIN_RIGHT - max_width = (Constants::REPORT_DOCX_WIDTH - margins) / 20 - - if dimension[0] > max_width - x = max_width - y = dimension[1] * max_width / dimension[0] - else - x = dimension[0] - y = dimension[1] - end - - { - width: x, - height: y, - align: align, - max_width: max_width - } - end - - def asset_image_preparing(asset) - return unless asset - - image_path = image_path(asset.file) - - dimension = FastImage.size(image_path) - x = dimension[0] - y = dimension[1] - if x > 300 - y = y * 300 / x - x = 300 - end - @docx.img image_path.split('&')[0] do - data asset.blob.download - width x - height y - end - end - def initial_document_load @docx.page_size do width Constants::REPORT_DOCX_WIDTH @@ -269,60 +53,4 @@ module Reports::Docx::PrivateMethods green: '2dbe61' } end - - def tiny_mce_table(table_data, options = {}) - docx_table = [] - scinote_url = @scinote_url - link_style = @link_style - table_data.css('tbody').first.children.each do |row| - docx_row = [] - next unless row.name == 'tr' - - row.children.each do |cell| - next unless cell.name == 'td' - - # Parse cell content - formated_cell = recursive_children(cell.children, [], nested_tables: true) - # Combine text elements to single paragraph - formated_cell = combine_docx_elements(formated_cell) - - docx_cell = Caracal::Core::Models::TableCellModel.new do |c| - formated_cell.each do |cell_content| - if cell_content[:type] == 'p' - Reports::Docx.render_p_element(c, cell_content, - scinote_url: scinote_url, link_style: link_style, skip_br: true) - elsif cell_content[:type] == 'table' - c.table formated_cell_content[:data], border_size: Constants::REPORT_DOCX_TABLE_BORDER_SIZE - elsif cell_content[:type] == 'image' - Reports::Docx.render_img_element(c, cell_content, table: { columns: row.children.length / 3 }) - end - end - end - docx_row.push(docx_cell) - end - docx_table.push(docx_row) - end - - if options[:nested_table] - docx_table - else - @docx.table docx_table, border_size: Constants::REPORT_DOCX_TABLE_BORDER_SIZE - end - end - - def image_path(attachment) - attachment.service_url - end - - def calculate_color_hsp(color) - return 255 if color.length != 7 - - color = color.delete('#').scan(/.{1,2}/) - rgb = color.map(&:hex) - Math.sqrt( - 0.299 * (rgb[0]**2) + - 0.587 * (rgb[1]**2) + - 0.114 * (rgb[2]**2) - ) - end end diff --git a/app/services/reports/docx_renderer.rb b/app/services/reports/docx_renderer.rb new file mode 100644 index 000000000..ba73b2511 --- /dev/null +++ b/app/services/reports/docx_renderer.rb @@ -0,0 +1,158 @@ +# frozen_string_literal: true + +module Reports + class DocxRenderer + def self.render_p_element(docx, element, options = {}) + docx.p do + element[:children].each do |text_el| + if text_el[:type] == 'text' + style = text_el[:style] || {} + text text_el[:value], style + text ' ' if text_el[:value] != '' + elsif text_el[:type] == 'br' && !options[:skip_br] + br + elsif text_el[:type] == 'a' + Reports::DocxRenderer.render_link_element(self, text_el, options) + end + end + end + end + + def self.render_link_element(node, link_item, options = {}) + scinote_url = options[:scinote_url] + link_style = options[:link_style] + + if link_item[:link] + link_url = Reports::Utils.link_prepare(scinote_url, link_item[:link]) + node.link link_item[:value], link_url, link_style + else + node.text link_item[:value], link_style + end + node.text ' ' if link_item[:value] != '' + end + + def self.render_img_element(docx, element, options = {}) + style = element[:style] + + if options[:table] + max_width = (style[:max_width] / options[:table][:columns].to_f) + if style[:width] > max_width + style[:height] = (max_width / style[:width].to_f) * style[:height] + style[:width] = max_width + end + end + + docx.img element[:data] do + data element[:blob].download + width style[:width] + height style[:height] + align style[:align] || :left + end + end + + def self.render_list_element(docx, element, options = {}) + bookmark_items = Reports::DocxRenderer.recursive_list_items_renderer(docx, element) + + bookmark_items.each_with_index do |(key, item), index| + if item[:type] == 'image' + docx.bookmark_start id: index, name: key + docx.p do + br + text item[:blob]&.filename.to_s + end + Reports::DocxRenderer.render_img_element(docx, item) + docx.bookmark_end id: index + elsif item[:type] == 'table' + docx.bookmark_start id: index, name: key + + # Bookmark won't work with table only, empty p element added + docx.p do + br + text '' + end + Reports::DocxRenderer.render_table_element(docx, item, options) + docx.bookmark_end id: index + end + end + end + + # rubocop:disable Metrics/BlockLength + def self.recursive_list_items_renderer(node, element, bookmark_items: {}) + node.public_send(element[:type]) do + element[:data].each do |values_array| + li do + values_array.each do |item| + case item + when Hash + if %w(ul ol li).include?(item[:type]) + Reports::DocxRenderer.recursive_list_items_renderer(self, item, bookmark_items: bookmark_items) + elsif %w(a).include?(item[:type]) + Reports::DocxRenderer.render_link_element(self, item) + elsif %w(image).include?(item[:type]) + bookmark_items[item[:bookmark_id]] = item + link I18n.t('projects.reports.renderers.lists.appended_image', + name: item[:blob]&.filename), item[:bookmark_id] do + internal true + end + elsif %w(table).include?(item[:type]) + bookmark_items[item[:bookmark_id]] = item + link I18n.t('projects.reports.renderers.lists.appended_table'), item[:bookmark_id] do + internal true + end + end + else + text item + end + end + end + end + end + bookmark_items + end + # rubocop:enable Metrics/BlockLength + + def self.render_table_element(docx, element, options = {}) + docx_table = [] + element[:data].each do |row| + docx_row = [] + row[:data].each do |cell| + docx_cell = Caracal::Core::Models::TableCellModel.new do |c| + cell.each do |content| + if content[:type] == 'p' + Reports::DocxRenderer.render_p_element(c, content, options.merge({ skip_br: true })) + elsif content[:type] == 'table' + Reports::DocxRenderer.render_table_element(c, content, options) + elsif content[:type] == 'image' + Reports::DocxRenderer.render_img_element(c, content, table: { columns: row.children.length / 3 }) + end + end + end + docx_row.push(docx_cell) + end + docx_table.push(docx_row) + end + docx.table docx_table, border_size: Constants::REPORT_DOCX_TABLE_BORDER_SIZE + end + + def self.render_asset_image(docx, asset) + return unless asset + + image_path = Reports::Utils.image_path(asset.file) + + dimension = FastImage.size(image_path) + return unless dimension + + x = dimension[0] + y = dimension[1] + if x > 300 + y = y * 300 / x + x = 300 + end + docx.img image_path.split('&')[0] do + data asset.blob.download + width x + height y + end + end + end +end diff --git a/app/services/reports/html_to_word_converter.rb b/app/services/reports/html_to_word_converter.rb new file mode 100644 index 000000000..b5fa20d35 --- /dev/null +++ b/app/services/reports/html_to_word_converter.rb @@ -0,0 +1,254 @@ +# frozen_string_literal: true + +module Reports + class HtmlToWordConverter + def initialize(document, options = {}) + @docx = document + @scinote_url = options[:scinote_url] + @link_style = options[:link_style] + end + + def html_to_word_converter(text) + html = Nokogiri::HTML(text) + raw_elements = recursive_children(html.css('body').children, []).compact + + # Combined raw text blocks in paragraphs + elements = combine_docx_elements(raw_elements) + + # Draw elements + elements.each do |elem| + if elem[:type] == 'p' + Reports::DocxRenderer.render_p_element(@docx, elem, scinote_url: @scinote_url, link_style: @link_style) + elsif elem[:type] == 'table' + Reports::DocxRenderer.render_table_element(@docx, elem) + elsif elem[:type] == 'newline' + style = elem[:style] || {} + # print heading if its heading + # Mixing heading with other style setting causes problems for Word + if %w(h1 h2 h3 h4 h5).include?(style[:style]) + @docx.public_send(style[:style], elem[:value]) + else + @docx.p elem[:value] do + align style[:align] + color style[:color] + bold style[:bold] + italic style[:italic] + end + end + elsif elem[:type] == 'image' + Reports::DocxRenderer.render_img_element(@docx, elem) + elsif %w(ul ol).include?(elem[:type]) + Reports::DocxRenderer.render_list_element(@docx, elem) + end + end + end + + private + + def combine_docx_elements(raw_elements) + # Word does not support some nested elements, move some elements to root level + elements = [] + temp_p = [] + raw_elements.each do |elem| + if %w(image newline table ol ul).include? elem[:type] + unless temp_p.empty? + elements.push(type: 'p', children: temp_p) + temp_p = [] + end + elements.push(elem) + elsif %w(br text a).include? elem[:type] + temp_p.push(elem) + end + end + elements.push(type: 'p', children: temp_p) + elements + end + + # Convert HTML structure to plain text structure + # rubocop:disable Metrics/BlockLength + def recursive_children(children, elements) + children.each do |elem| + if elem.class == Nokogiri::XML::Text + next if elem.text.strip == ' ' # Invisible symbol + + style = paragraph_styling(elem.parent) + type = (style[:align] && style[:align] != :justify) || style[:style] ? 'newline' : 'text' + + text = smart_annotation_check(elem) + + elements.push( + type: type, + value: text.strip.delete(' '), # Invisible symbol + style: style + ) + next + end + + if elem.name == 'br' + elements.push(type: 'br') + next + end + + if elem.name == 'img' + elements.push(img_element(elem)) + next + end + + if elem.name == 'a' + elements.push(link_element(elem)) + next + end + + if elem.name == 'table' + elements.push(tiny_mce_table_element(elem)) + next + end + + if %w(ul ol).include?(elem.name) + elements.push(list_element(elem)) + next + end + elements = recursive_children(elem.children, elements) if elem.children + end + elements + end + + # rubocop:enable Metrics/BlockLength + + def img_element(elem) + return unless elem.attributes['data-mce-token'] + + image = TinyMceAsset.find_by(id: Base62.decode(elem.attributes['data-mce-token'].value)) + return unless image + + image_path = Reports::Utils.image_path(image.image) + dimension = FastImage.size(image_path) + + return unless dimension + + style = image_styling(elem, dimension) + + { type: 'image', data: image_path.split('&')[0], blob: image.blob, style: style } + end + + def link_element(elem) + text = elem.text + link = elem.attributes['href'].value if elem.attributes['href'] + if elem.attributes['class']&.value == 'record-info-link' + link = nil + text = "##{text}" + end + text = "##{text}" if elem.parent.attributes['class']&.value == 'atwho-inserted' + text = "@#{text}" if elem.attributes['class']&.value == 'atwho-user-popover' + { + type: 'a', + value: text, + link: link + } + end + + def list_element(list_element) + data_array = list_element.children.select { |n| %w(li ul ol a img).include?(n.name) }.map do |li_child| + li_child.children.map do |item| + if item.is_a? Nokogiri::XML::Text + item.text.chomp + elsif %w(ul ol).include?(item.name) + list_element(item) + elsif %w(a).include?(item.name) + link_element(item) + elsif %w(img).include?(item.name) + img_element(item)&.merge(bookmark_id: SecureRandom.hex) + elsif %w(table).include?(item.name) + tiny_mce_table_element(item).merge(bookmark_id: SecureRandom.hex) + end + end.reject(&:blank?) + end + { type: list_element.name, data: data_array } + end + + def smart_annotation_check(elem) + return "[#{elem.text}]" if elem.parent.attributes['class']&.value == 'sa-type' + + elem.text + end + + # Prepare style for text + def paragraph_styling(elem) + style = elem.attributes['style'] + result = {} + result[:style] = elem.name if elem.name.include? 'h' + result[:bold] = true if elem.name == 'strong' + result[:italic] = true if elem.name == 'em' + style_keys = %w(text-align color) + + if style + style_keys.each do |key| + style_el = style.value.split(';').select { |i| (i.include? key) }[0] + next unless style_el + + value = style_el.split(':')[1].strip if style_el + if key == 'text-align' + result[:align] = value.to_sym + elsif key == 'color' && Reports::Utils.calculate_color_hsp(value) < 190 + result[:color] = value.delete('#') + end + end + end + result + end + + # Prepare style for images + def image_styling(elem, dimension) + dimension[0] = elem.attributes['width'].value.to_i if elem.attributes['width'] + dimension[1] = elem.attributes['height'].value.to_i if elem.attributes['height'] + + if elem.attributes['style'] + align = if elem.attributes['style'].value.include? 'margin-right' + :center + elsif elem.attributes['style'].value.include? 'float: right' + :right + else + :left + end + end + + margins = Constants::REPORT_DOCX_MARGIN_LEFT + Constants::REPORT_DOCX_MARGIN_RIGHT + max_width = (Constants::REPORT_DOCX_WIDTH - margins) / 20 + + if dimension[0] > max_width + x = max_width + y = dimension[1] * max_width / dimension[0] + else + x = dimension[0] + y = dimension[1] + end + + { + width: x, + height: y, + align: align, + max_width: max_width + } + end + + def tiny_mce_table_element(table_element) + # array of elements + rows = table_element.css('tbody').first.children.map do |row| + next unless row.name == 'tr' + + cells = row.children.map do |cell| + next unless cell.name == 'td' + + # Parse cell content + formated_cell = recursive_children(cell.children, []) + + # Combine text elements to single paragraph + formated_cell = combine_docx_elements(formated_cell) + formated_cell + end.reject(&:blank?) + { type: 'tr', data: cells } + end.reject(&:blank?) + { type: 'table', data: rows } + end + end +end diff --git a/app/services/reports/utils.rb b/app/services/reports/utils.rb new file mode 100644 index 000000000..b61f1cf1c --- /dev/null +++ b/app/services/reports/utils.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Reports + class Utils + def self.link_prepare(scinote_url, link) + link[0] == '/' ? scinote_url + link : link + end + + def self.image_path(attachment) + attachment.service_url + end + + def self.calculate_color_hsp(color) + return 255 if color.length != 7 + + color = color.delete('#').scan(/.{1,2}/) + rgb = color.map(&:hex) + Math.sqrt( + 0.299 * (rgb[0]**2) + + 0.587 * (rgb[1]**2) + + 0.114 * (rgb[2]**2) + ) + end + end +end diff --git a/config/locales/en.yml b/config/locales/en.yml index a2f4d4a50..ba2acfbc2 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -453,6 +453,10 @@ en: nothing_selected: "Nothing selected" generate_PDF: generated_on: "Report generated by SciNote on: %{timestamp}" + renderers: + lists: + appended_image: "Appended image - %{name}" + appended_table: "Appended table" elements: modals: project_contents: diff --git a/spec/services/reports/html_to_word_converter_spec.rb b/spec/services/reports/html_to_word_converter_spec.rb new file mode 100644 index 000000000..1393d7892 --- /dev/null +++ b/spec/services/reports/html_to_word_converter_spec.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Reports::HtmlToWordConverter do + let(:user) { create :user } + let(:team) { create :team } + let(:docx) { double('docx') } + let(:report) { described_class.new(docx) } + + describe 'html_list' do + let(:text) do + '' + end + let(:xml_elements) { Nokogiri::HTML(text).css('body').children.first } + let(:result) do + { + type: 'ul', + data: [%w(1), + ['2', { type: 'ul', data: [%w(one), ['two', { type: 'ol', data: [%w(uno), %w(due)] }]] }], + %w(3), %w(4), %w(5)] + } + end + it '' do + expect(report.__send__(:list_element, xml_elements)).to be == result + end + end + + describe '.tiny_mce_table_element' do + let(:text) do + # rubocop:disable Layout/LineLength + '
12
34
' + # rubocop:enable Layout/LineLength + end + let(:xml_elements) { Nokogiri::HTML(text).css('body').children.first } + let(:result) do + { + data: [ + { + data: [ + [{ children: [{ style: {}, type: 'text', value: '1' }], type: 'p' }], + [{ children: [{ style: {}, type: 'text', value: '2' }], type: 'p' }] + ], + type: 'tr' + }, + { + data: [ + [{ children: [{ style: {}, type: 'text', value: '3' }], type: 'p' }], + [{ children: [{ style: {}, type: 'text', value: '4' }], type: 'p' }] + ], + type: 'tr' + } + ], + type: 'table' + } + end + + it '' do + expect(report.__send__(:tiny_mce_table_element, xml_elements)).to be == result + end + end +end