From d023f96aa9c05f19bc135f483c8003bea9f9efb3 Mon Sep 17 00:00:00 2001 From: Zardshard <101750307+Zardshard@users.noreply.github.com> Date: Thu, 17 Mar 2022 07:13:55 -0400 Subject: [PATCH] Added support for highlighted text --- lib/docx/containers/text_run.rb | 15 +++- spec/docx/document_spec.rb | 131 +++++++++++++++++++------------- spec/fixtures/formatting.docx | Bin 12477 -> 11080 bytes 3 files changed, 92 insertions(+), 54 deletions(-) diff --git a/lib/docx/containers/text_run.rb b/lib/docx/containers/text_run.rb index 59be9d3..7fb4dc7 100755 --- a/lib/docx/containers/text_run.rb +++ b/lib/docx/containers/text_run.rb @@ -10,7 +10,8 @@ class TextRun DEFAULT_FORMATTING = { italic: false, bold: false, - underline: false + underline: false, + highlight: 'none' } def self.tag @@ -60,7 +61,8 @@ def parse_formatting { italic: !@node.xpath('.//w:i').empty?, bold: !@node.xpath('.//w:b').empty?, - underline: !@node.xpath('.//w:u').empty? + underline: !@node.xpath('.//w:u').empty?, + highlight: @node.xpath('.//w:highlight').first&.[]('w:val') || 'none' } end @@ -75,6 +77,7 @@ def to_html html = html_tag(:strong, content: html) if bolded? styles = {} styles['text-decoration'] = 'underline' if underlined? + styles['background-color'] = highlight unless highlight == 'none' # No need to be granular with font size down to the span level if it doesn't vary. styles['font-size'] = "#{font_size}pt" if font_size != @font_size html = html_tag(:span, content: html, styles: styles) unless styles.empty? @@ -94,6 +97,14 @@ def underlined? @formatting[:underline] end + def highlighted? + @formatting[:highlight] != 'none' + end + + def highlight + @formatting[:highlight] + end + def hyperlink? @node.name == 'hyperlink' end diff --git a/spec/docx/document_spec.rb b/spec/docx/document_spec.rb index 0828cd2..a242235 100755 --- a/spec/docx/document_spec.rb +++ b/spec/docx/document_spec.rb @@ -7,7 +7,7 @@ describe Docx::Document do before(:all) do @fixtures_path = 'spec/fixtures' - @formatting_line_count = 13 # number of lines the formatting.docx file has + @formatting_line_count = 14 # number of lines the formatting.docx file has end describe '#open' do @@ -189,7 +189,8 @@ @only_italic = @default_formatting.merge italic: true @only_bold = @default_formatting.merge bold: true @only_underline = @default_formatting.merge underline: true - @all_formatted = @default_formatting.merge italic: true, bold: true, underline: true + @only_highlight = @default_formatting.merge highlight: 'yellow' + @all_formatted = @default_formatting.merge italic: true, bold: true, underline: true, highlight: 'yellow' end it 'should have the correct text' do @@ -198,26 +199,28 @@ expect(@doc.paragraphs[1].text).to eq('Italic') expect(@doc.paragraphs[2].text).to eq('Bold') expect(@doc.paragraphs[3].text).to eq('Underline') - expect(@doc.paragraphs[4].text).to eq('Normal') - expect(@doc.paragraphs[5].text).to eq('This is a sentence with all formatting options in the middle of the sentence.') - expect(@doc.paragraphs[6].text).to eq('This is a centered paragraph.') - expect(@doc.paragraphs[7].text).to eq('This paragraph is aligned left.') - expect(@doc.paragraphs[8].text).to eq('This paragraph is aligned right.') - expect(@doc.paragraphs[9].text).to eq('This paragraph is 14 points.') - expect(@doc.paragraphs[10].text).to eq('This paragraph has a word at 16 points.') - expect(@doc.paragraphs[11].text).to eq('This sentence has different formatting in different places.') - expect(@doc.paragraphs[12].text).to eq('This sentence has a hyperlink.') + expect(@doc.paragraphs[4].text).to eq('Highlight') + expect(@doc.paragraphs[5].text).to eq('Normal') + expect(@doc.paragraphs[6].text).to eq('This is a sentence with all formatting options in the middle of the sentence.') + expect(@doc.paragraphs[7].text).to eq('This is a centered paragraph.') + expect(@doc.paragraphs[8].text).to eq('This paragraph is aligned left.') + expect(@doc.paragraphs[9].text).to eq('This paragraph is aligned right.') + expect(@doc.paragraphs[10].text).to eq('This paragraph is 14 points.') + expect(@doc.paragraphs[11].text).to eq('This paragraph has a word at 16 points.') + expect(@doc.paragraphs[12].text).to eq('This sentence has different formatting in different places.') + expect(@doc.paragraphs[13].text).to eq('This sentence has a hyperlink.') end it 'should contain a paragraph with multiple text runs' do end it 'should detect normal formatting' do - [0, 4].each do |i| + [0, 5].each do |i| expect(@formatting[i][0]).to eq(@default_formatting) expect(@doc.paragraphs[i].text_runs[0].italicized?).to eq(false) expect(@doc.paragraphs[i].text_runs[0].bolded?).to eq(false) expect(@doc.paragraphs[i].text_runs[0].underlined?).to eq(false) + expect(@doc.paragraphs[i].text_runs[0].highlighted?).to eq(false) end end @@ -226,6 +229,7 @@ expect(@doc.paragraphs[1].text_runs[0].italicized?).to eq(true) expect(@doc.paragraphs[1].text_runs[0].bolded?).to eq(false) expect(@doc.paragraphs[1].text_runs[0].underlined?).to eq(false) + expect(@doc.paragraphs[1].text_runs[0].highlighted?).to eq(false) end it 'should detect bold formatting' do @@ -233,6 +237,7 @@ expect(@doc.paragraphs[2].text_runs[0].italicized?).to eq(false) expect(@doc.paragraphs[2].text_runs[0].bolded?).to eq(true) expect(@doc.paragraphs[2].text_runs[0].underlined?).to eq(false) + expect(@doc.paragraphs[2].text_runs[0].highlighted?).to eq(false) end it 'should detect underline formatting' do @@ -240,41 +245,54 @@ expect(@doc.paragraphs[3].text_runs[0].italicized?).to eq(false) expect(@doc.paragraphs[3].text_runs[0].bolded?).to eq(false) expect(@doc.paragraphs[3].text_runs[0].underlined?).to eq(true) + expect(@doc.paragraphs[3].text_runs[0].highlighted?).to eq(false) + end + + it 'should detect highlight formatting' do + expect(@formatting[4][0]).to eq(@only_highlight) + expect(@doc.paragraphs[4].text_runs[0].italicized?).to eq(false) + expect(@doc.paragraphs[4].text_runs[0].bolded?).to eq(false) + expect(@doc.paragraphs[4].text_runs[0].underlined?).to eq(false) + expect(@doc.paragraphs[4].text_runs[0].highlighted?).to eq(true) + expect(@doc.paragraphs[4].text_runs[0].highlight).to eq('yellow') end it 'should detect mixed formatting' do - expect(@formatting[5][0]).to eq(@default_formatting) - expect(@doc.paragraphs[5].text_runs[0].italicized?).to eq(false) - expect(@doc.paragraphs[5].text_runs[0].bolded?).to eq(false) - expect(@doc.paragraphs[5].text_runs[0].underlined?).to eq(false) + expect(@formatting[6][0]).to eq(@default_formatting) + expect(@doc.paragraphs[6].text_runs[0].italicized?).to eq(false) + expect(@doc.paragraphs[6].text_runs[0].bolded?).to eq(false) + expect(@doc.paragraphs[6].text_runs[0].underlined?).to eq(false) + expect(@doc.paragraphs[6].text_runs[0].highlighted?).to eq(false) - expect(@formatting[5][1]).to eq(@all_formatted) - expect(@doc.paragraphs[5].text_runs[1].italicized?).to eq(true) - expect(@doc.paragraphs[5].text_runs[1].bolded?).to eq(true) - expect(@doc.paragraphs[5].text_runs[1].underlined?).to eq(true) + expect(@formatting[6][1]).to eq(@all_formatted) + expect(@doc.paragraphs[6].text_runs[1].italicized?).to eq(true) + expect(@doc.paragraphs[6].text_runs[1].bolded?).to eq(true) + expect(@doc.paragraphs[6].text_runs[1].underlined?).to eq(true) + expect(@doc.paragraphs[6].text_runs[1].highlight).to eq('yellow') - expect(@formatting[5][2]).to eq(@default_formatting) - expect(@doc.paragraphs[5].text_runs[2].italicized?).to eq(false) - expect(@doc.paragraphs[5].text_runs[2].bolded?).to eq(false) - expect(@doc.paragraphs[5].text_runs[2].underlined?).to eq(false) + expect(@formatting[6][2]).to eq(@default_formatting) + expect(@doc.paragraphs[6].text_runs[2].italicized?).to eq(false) + expect(@doc.paragraphs[6].text_runs[2].bolded?).to eq(false) + expect(@doc.paragraphs[6].text_runs[2].underlined?).to eq(false) + expect(@doc.paragraphs[6].text_runs[2].highlighted?).to eq(false) end it 'should detect centered paragraphs' do - expect(@doc.paragraphs[5].aligned_center?).to eq(false) - expect(@doc.paragraphs[6].aligned_center?).to eq(true) - expect(@doc.paragraphs[7].aligned_center?).to eq(false) + expect(@doc.paragraphs[6].aligned_center?).to eq(false) + expect(@doc.paragraphs[7].aligned_center?).to eq(true) + expect(@doc.paragraphs[8].aligned_center?).to eq(false) end it 'should detect left justified paragraphs' do - expect(@doc.paragraphs[6].aligned_left?).to eq(false) - expect(@doc.paragraphs[7].aligned_left?).to eq(true) - expect(@doc.paragraphs[8].aligned_left?).to eq(false) + expect(@doc.paragraphs[7].aligned_left?).to eq(false) + expect(@doc.paragraphs[8].aligned_left?).to eq(true) + expect(@doc.paragraphs[9].aligned_left?).to eq(false) end it 'should detect right justified paragraphs' do - expect(@doc.paragraphs[7].aligned_right?).to eq(false) - expect(@doc.paragraphs[8].aligned_right?).to eq(true) - expect(@doc.paragraphs[9].aligned_right?).to eq(false) + expect(@doc.paragraphs[8].aligned_right?).to eq(false) + expect(@doc.paragraphs[9].aligned_right?).to eq(true) + expect(@doc.paragraphs[10].aligned_right?).to eq(false) end # ECMA-376 Office Open XML spec (4th edition), 17.3.2.38, size is @@ -282,15 +300,15 @@ # http://www.ecma-international.org/publications/standards/Ecma-376.htm it 'should return proper font size for paragraphs' do expect(@doc.font_size).to eq 11 - expect(@doc.paragraphs[5].font_size).to eq 11 - paragraph = @doc.paragraphs[9] + expect(@doc.paragraphs[6].font_size).to eq 11 + paragraph = @doc.paragraphs[10] expect(paragraph.font_size).to eq 14 expect(paragraph.text_runs[0].font_size).to eq 14 end it 'should return proper font size for runs' do expect(@doc.font_size).to eq 11 - paragraph = @doc.paragraphs[10] + paragraph = @doc.paragraphs[11] expect(paragraph.font_size).to eq 11 text_runs = paragraph.text_runs expect(text_runs[0].font_size).to eq 11 @@ -301,7 +319,7 @@ end it 'should return changed value for runs' do - paragraph = @doc.paragraphs[10] + paragraph = @doc.paragraphs[11] text_runs = paragraph.text_runs tr = text_runs[0] @@ -362,9 +380,9 @@ describe 'outputting html' do before do @doc = Docx::Document.open(@fixtures_path + '/formatting.docx') - @formatted_line = @doc.paragraphs[5] + @formatted_line = @doc.paragraphs[6] @p_regex = /(^\)\w+)(\<\/p>$)/ - @span_regex = /(\)\w+)(<\/span>)/ + @span_regex = /(\)\w+)(<\/span>)/ @em_regex = /(\)\w+)(\<\/em\>)/ @strong_regex = /(\)\w+)(\<\/strong\>)/ @anchor_tag_regex = /\(.+)\<\/a>/ @@ -396,16 +414,21 @@ expect(scan.first).to eq 'style="text-decoration:underline;"' end + it 'should highlight highlighted text' do + scan = @doc.paragraphs[4].to_html.scan(/\]+)/).flatten + expect(scan.first).to eq 'style="background-color:yellow;"' + end + it 'should justify paragraphs' do regex = /^]+style\=\"([^\"]+).+(<\/p>)/ - scan = @doc.paragraphs[9].to_html.scan(regex).flatten + scan = @doc.paragraphs[10].to_html.scan(regex).flatten expect(scan.first).to eq '' expect(scan[1].split(';').include?('font-size:14pt')).to eq(true) @@ -413,14 +436,14 @@ it 'should set font size on styled text runs' do regex = /(\]+style\=\"([^\"]+)[^\<]+(<\/span>)/ - scan = @doc.paragraphs[10].to_html.scan(regex).flatten + scan = @doc.paragraphs[11].to_html.scan(regex).flatten expect(scan.first).to eq '' expect(scan[1].split(';').include?('font-size:16pt')).to eq(true) end it 'should properly highlight different text in different places in a sentence' do - paragraph = @doc.paragraphs[11] + paragraph = @doc.paragraphs[12] scan = paragraph.to_html.scan(@em_regex).flatten expect(scan.first).to eq '' @@ -429,12 +452,16 @@ expect(scan.first).to eq '' expect(scan[1]).to eq 'formatting' - scan = paragraph.to_html.scan(@span_regex).flatten - expect(scan.first).to eq '' - expect(scan[1]).to eq 'different' - scan = paragraph.to_html.scan(/\]+)/).flatten - expect(scan.first).to eq 'style="text-decoration:underline;"' + scan = paragraph.to_html.scan(@span_regex) + expect(scan[0].first).to eq '' + expect(scan[1].first).to eq '' + expect(scan[0][1]).to eq 'different' + expect(scan[1][1]).to eq 'places' + scan = paragraph.to_html.scan(/\]+)/) + expect(scan[0].first).to eq 'style="text-decoration:underline;"' + expect(scan[1].first).to eq 'style="background-color:yellow;"' end it 'should output an entire document as html fragment' do @@ -442,7 +469,7 @@ end it 'should output styled html' do - expect(@formatted_line.to_html.scan('all').size).to eq 1 + expect(@formatted_line.to_html.scan('all').size).to eq 1 end it 'should join paragraphs with newlines' do diff --git a/spec/fixtures/formatting.docx b/spec/fixtures/formatting.docx index 53e21d9dc3a62215a521f01c4778315eadc4e2db..3afc21e320ebf929270b942bab76ce3b18218374 100644 GIT binary patch literal 11080 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 XO%(*Fdi*TFLj!a}6DD`2pTGVe?~P=U literal 12477 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 a&ly@l8U}10KPsRR0qx*jf^DWBzy1%)Ef_xl