mirror of
https://github.com/scinote-eln/scinote-web.git
synced 2025-03-19 11:24:14 +08:00
Merge pull request #2899 from biosistemika/release/1.20.2
Release/1.20.2
This commit is contained in:
commit
c8083f730c
17 changed files with 520 additions and 330 deletions
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
158
app/services/reports/docx_renderer.rb
Normal file
158
app/services/reports/docx_renderer.rb
Normal file
|
@ -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
|
254
app/services/reports/html_to_word_converter.rb
Normal file
254
app/services/reports/html_to_word_converter.rb
Normal file
|
@ -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
|
25
app/services/reports/utils.rb
Normal file
25
app/services/reports/utils.rb
Normal file
|
@ -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
|
|
@ -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:
|
||||
|
|
63
spec/services/reports/html_to_word_converter_spec.rb
Normal file
63
spec/services/reports/html_to_word_converter_spec.rb
Normal file
|
@ -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
|
||||
'<body><ul><li>1</li><li>2<ul><li>one</li><li>two<ol><li>uno</li><li>due</li>'\
|
||||
'</ol></li></ul></li><li>3</li><li>4</li><li>5</li></ul></body>'
|
||||
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
|
||||
'<body><table style="border-collapse: collapse; width: 100%; height: 28px;" border="1" data-mce-style="border-collapse: collapse; width: 100%; height: 28px;"><tbody><tr style="height: 10px;"><td style="width: 50%; height: 10px;">1</td><td style="width: 50%; height: 10px;">2</td></tr><tr style="height: 18px;"><td style="width: 50%; height: 18px;">3</td><td style="width: 50%; height: 18px;">4</td></tr></tbody></table></body>'
|
||||
# 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
|
Loading…
Add table
Reference in a new issue