Skip to content
Browse files

Extracting text from html if it can't be had through some text/plain …

…parts. First pass.
  • Loading branch information...
1 parent 156783c commit c09111caef45fa301d20a7a441bec283b71acc32 @aiwilliams committed Feb 12, 2009
View
1 CHANGELOG
@@ -7,6 +7,7 @@
* MList::List implementors may now answer the footer content to be appended to the bottom of the text/plain part of messages. [aiwilliams]
* List footers are stripped from text/plain part of messages before being delivered. [aiwilliams]
* Observers of MList models which are defined in client applications now work without special instruction. [aiwilliams]
+* A first pass implementation for converting html to text using Hpricot. [aiwilliams]
*0.1.4 [] (January 7, 2009)
View
1 README
@@ -54,6 +54,7 @@ You love Ruby. You want MList.
You'll need some gems.
+* hpricot
* uuid (macaddr also)
* tmail
* activesupport
View
36 TODO
@@ -0,0 +1,36 @@
+#html_to_text
+
+ADDRESS - Address
+BLOCKQUOTE - Block quotation
+
+DIV - Generic block-level container
+DL - Definition list
+FIELDSET - Form control group
+FORM - Interactive form
+H1 - Level-one heading
+H2 - Level-two heading
+H3 - Level-three heading
+H4 - Level-four heading
+H5 - Level-five heading
+H6 - Level-six heading
+HR - Horizontal rule
+OL - Ordered list
+P - Paragraph
+PRE - Preformatted text
+
+TABLE - Table
+ output TRs as lines, csv the TDs
+
+UL - Unordered list
+
+DD - Definition description
+DT - Definition term
+LI - List item
+
+
+a (Links) - Link to Somewhere[1] ---- [1] http://
+b (Bold) - *bold*
+i (Italic) - _italic_
+strong - *strong*
+em (emphasis) - _emphasis_
+u (underline) - probably do nothing, maybe something like em
View
1 lib/mlist.rb
@@ -1,5 +1,6 @@
require 'uuid'
require 'tmail'
+require 'hpricot'
require 'activesupport'
require 'activerecord'
View
81 lib/mlist/util/email_helpers.rb
@@ -1,6 +1,83 @@
module MList
module Util
+ class HtmlTextExtraction
+ def initialize(html)
+ @doc = Hpricot(html)
+ end
+
+ def execute
+ @text, @anchors = '', []
+ @doc.each_child do |node|
+ extract_text_from_node(node) if Hpricot::Elem::Trav === node
+ end
+ @text.strip!
+ unless @anchors.empty?
+ refs = []
+ @anchors.each_with_index do |href, i|
+ refs << "[#{i+1}] #{href}"
+ end
+ @text << "\n\n--\n#{refs.join("\n")}"
+ end
+ @text
+ end
+
+ def extract_text_from_node(node)
+ case node.name
+ when 'head'
+ when 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'
+ @text << node.inner_text
+ @text << "\n\n"
+ when 'br'
+ @text << "\n"
+ when 'ol'
+ node.children_of_type('li').each_with_index do |li, i|
+ @text << " #{i+1}. #{li.inner_text}"
+ @text << "\n\n"
+ end
+ when 'ul'
+ node.children_of_type('li').each do |li|
+ @text << " * #{li.inner_text.strip}"
+ @text << "\n\n"
+ end
+ when 'strong'
+ @text << "*#{node.inner_text}*"
+ when 'em'
+ @text << "_#{node.inner_text}_"
+ when 'dl'
+ node.traverse_element('dt', 'dd') do |dt_dd|
+ extract_text_from_node(dt_dd)
+ end
+ when 'a'
+ @anchors << node['href']
+ extract_text_from_text_node(node)
+ @text << "[#{@anchors.size}]"
+ when 'p', 'dt', 'dd'
+ extract_text_from_children(node)
+ @text.rstrip!
+ @text << "\n\n"
+ else
+ extract_text_from_children(node)
+ end
+ end
+
+ def extract_text_from_children(elem)
+ elem.each_child do |node|
+ case node
+ when Hpricot::Text::Trav
+ extract_text_from_text_node(node)
+ when Hpricot::Elem::Trav
+ extract_text_from_node(node)
+ end
+ end
+ end
+
+ def extract_text_from_text_node(node)
+ text = @text.end_with?("\n") ? node.inner_text.lstrip : node.inner_text
+ @text << text.gsub(/\s{2,}/, ' ').sub(/\n/, '')
+ end
+ end
+
module EmailHelpers
def sanitize_header(charset, name, *values)
header_sanitizer(name).call(charset, *values)
@@ -10,6 +87,10 @@ def header_sanitizer(name)
Util.default_header_sanitizers[name]
end
+ def html_to_text(html)
+ HtmlTextExtraction.new(html).execute
+ end
+
def normalize_new_lines(text)
text.to_s.gsub(/\r\n?/, "\n")
end
View
26 lib/mlist/util/tmail_methods.rb
@@ -33,7 +33,15 @@ def mailer
end
def text
- returning('') {|content| extract_text_content(tmail, content)}
+ text_content = ''
+ extract_text_content(tmail, text_content)
+ return text_content unless text_content.blank?
+
+ html_content = ''
+ extract_html_content(tmail, html_content)
+ return html_to_text(html_content) unless html_content.blank?
+
+ return nil
end
# Answers the first text/plain part it can find, the tmail itself if
@@ -49,15 +57,21 @@ def text_plain_part(part = tmail)
end
private
+ def extract_html_content(part, collector)
+ case part.content_type
+ when 'text/html'
+ collector << part.body.strip
+ when 'multipart/alternative', 'multipart/mixed', 'multipart/related'
+ part.parts.each {|part| extract_html_content(part, collector)}
+ end
+ end
+
def extract_text_content(part, collector)
case part.content_type
when 'text/plain'
collector << part.body.strip
- when 'multipart/alternative'
- text_part = part.parts.detect {|part| part.content_type == 'text/plain'}
- collector << text_part.body.strip if text_part
- when 'multipart/mixed', 'multipart/related'
- part.parts.each {|mixed_part| extract_text_content(mixed_part, collector)}
+ when 'multipart/alternative', 'multipart/mixed', 'multipart/related'
+ part.parts.each {|part| extract_text_content(part, collector)}
end
end
end
View
2 mlist.gemspec
@@ -9,7 +9,7 @@ Gem::Specification.new do |s|
s.date = %q{2009-02-12}
s.description = %q{A Ruby mailing list library designed to be integrated into other applications.}
s.email = %q{adam@thewilliams.ws}
- s.files = ["CHANGELOG", "Rakefile", "README", "VERSION.yml", "lib/mlist", "lib/mlist/email.rb", "lib/mlist/email_post.rb", "lib/mlist/email_server", "lib/mlist/email_server/base.rb", "lib/mlist/email_server/default.rb", "lib/mlist/email_server/fake.rb", "lib/mlist/email_server/pop.rb", "lib/mlist/email_server/smtp.rb", "lib/mlist/email_server.rb", "lib/mlist/email_subscriber.rb", "lib/mlist/list.rb", "lib/mlist/mail_list.rb", "lib/mlist/manager", "lib/mlist/manager/database.rb", "lib/mlist/manager/notifier.rb", "lib/mlist/manager.rb", "lib/mlist/message.rb", "lib/mlist/server.rb", "lib/mlist/thread.rb", "lib/mlist/util", "lib/mlist/util/email_helpers.rb", "lib/mlist/util/header_sanitizer.rb", "lib/mlist/util/quoting.rb", "lib/mlist/util/tmail_builder.rb", "lib/mlist/util/tmail_methods.rb", "lib/mlist/util.rb", "lib/mlist.rb", "lib/pop_ssl.rb", "rails/init.rb"]
+ s.files = ["CHANGELOG", "Rakefile", "README", "TODO", "VERSION.yml", "lib/mlist", "lib/mlist/email.rb", "lib/mlist/email_post.rb", "lib/mlist/email_server", "lib/mlist/email_server/base.rb", "lib/mlist/email_server/default.rb", "lib/mlist/email_server/fake.rb", "lib/mlist/email_server/pop.rb", "lib/mlist/email_server/smtp.rb", "lib/mlist/email_server.rb", "lib/mlist/email_subscriber.rb", "lib/mlist/list.rb", "lib/mlist/mail_list.rb", "lib/mlist/manager", "lib/mlist/manager/database.rb", "lib/mlist/manager/notifier.rb", "lib/mlist/manager.rb", "lib/mlist/message.rb", "lib/mlist/server.rb", "lib/mlist/thread.rb", "lib/mlist/util", "lib/mlist/util/email_helpers.rb", "lib/mlist/util/header_sanitizer.rb", "lib/mlist/util/quoting.rb", "lib/mlist/util/tmail_builder.rb", "lib/mlist/util/tmail_methods.rb", "lib/mlist/util.rb", "lib/mlist.rb", "lib/pop_ssl.rb", "rails/init.rb"]
s.homepage = %q{http://github.com/aiwilliams/mlist}
s.require_paths = ["lib"]
s.rubygems_version = %q{1.3.1}
View
335 spec/fixtures/email/content_types/multipart_related_no_text_plain
@@ -0,0 +1,335 @@
+From: Adam Williams <adam@nomail.net>
+To: list_one@example.com
+Subject: Testing content types
+Message-Id: <659E5160-32FD-4971-BF5B-8E973A08C541@gmail.com>
+Content-Type: multipart/related; boundary=Apple-Mail-30-549118182; type="text/html"
+Mime-Version: 1.0 (Apple Message framework v930.3)
+Date: Wed, 11 Feb 2009 21:47:52 -0500
+X-Mailer: Apple Mail (2.930.3)
+
+
+--Apple-Mail-30-549118182
+Content-Type: text/html;
+ charset=US-ASCII
+Content-Transfer-Encoding: quoted-printable
+
+<html><body style=3D"word-wrap: break-word; -webkit-nbsp-mode: space; =
+-webkit-line-break: after-white-space; ">I don't really have much to =
+say, so I'm going to share some random things I saw today:<br><br>I saw =
+this guy on twitter.com, and he looks pretty chill:<span =
+class=3D"Apple-string-attachment"><object =
+type=3D"application/x-apple-msg-attachment" =
+data=3D"cid:9EE68BDE-8412-47F2-BF57-A4A2D3CED9B0" height=3D"73" =
+width=3D"77"></object></span><br><br>I found this sweet url, and it's =
+not dirty!: <span class=3D"Apple-string-attachment"><object =
+type=3D"application/x-apple-msg-attachment" =
+data=3D"cid:6F676630-55B8-49AA-B645-C0EAD99F00BC" height=3D"36" =
+width=3D"228"></object></span><br><br>I found out that if I call our =
+Skype phone from Skype on my laptop, my laptop will give me the ability =
+to answer the call I am placing. Freaky!<br><br>Here is what my rating =
+star widget looks like:<span class=3D"Apple-string-attachment"><object =
+type=3D"application/x-apple-msg-attachment" =
+data=3D"cid:A73DA1C5-C6FD-452E-8709-9774DCE18A17" height=3D"29" =
+width=3D"242"></object></span><br><br><blockquote type=3D"cite">What's =
+with the dashes and tildes?<br></blockquote><br>Yeah, what is going on =
+with that. They don't even match.<br><blockquote =
+type=3D"cite">-~----~~----~----~----~----~---~~-~----~------~--~-~-<br></b=
+lockquote>vs<br><blockquote =
+type=3D"cite">--~--~---~~----~--~----~-----~~~----~---~---~--~-~--~<br></b=
+lockquote><br><br>Good job with this!<br><br>-Steve</body></html>=
+
+--Apple-Mail-30-549118182
+Content-Disposition: inline;
+ filename=the_wackness_008_bigger.JPG
+Content-Transfer-Encoding: base64
+Content-Type: image/pjpeg;
+ x-unix-mode=0666;
+ name="the_wackness_008_bigger.JPG"
+Content-Id: <9EE68BDE-8412-47F2-BF57-A4A2D3CED9B0>
+
+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
+lSsUnFf/2Q==
+
+--Apple-Mail-30-549118182
+Content-Disposition: inline;
+ filename=pastedGraphic.png
+Content-Transfer-Encoding: base64
+Content-Type: image/png;
+ x-unix-mode=0666;
+ name="pastedGraphic.png"
+Content-Id: <6F676630-55B8-49AA-B645-C0EAD99F00BC>
+
+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
+TkSuQmCC
+
+--Apple-Mail-30-549118182
+Content-Disposition: inline;
+ filename=pastedGraphic.png
+Content-Transfer-Encoding: base64
+Content-Type: image/png;
+ x-unix-mode=0666;
+ name="pastedGraphic.png"
+Content-Id: <A73DA1C5-C6FD-452E-8709-9774DCE18A17>
+
+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
+QmCC
+
+--Apple-Mail-30-549118182--
View
70 spec/fixtures/html/real_life
@@ -0,0 +1,70 @@
+<html>
+<head>
+ <title>This Rocks!</title>
+</head>
+<body>
+<h1>Heading 1</h1>
+<p>
+ Lorem ipsum <strong>dolor sit amet</strong>, consectetuer
+ <em>adipiscing</em> elit.<br />
+ End of content for heading 1
+</p>
+<h2>Heading 2</h2>
+<p>
+ content 2
+</p>
+<ul>
+ <li>Apples
+ </li>
+ <li>Oranges</li>
+ <li>Bananas</li>
+</ul>
+<h3>Heading 3</h3>
+<p>
+ content 3
+ <a href="http://somewhere.com">Somewhere</a>
+</p>
+<ol>
+ <li>Cowboys</li>
+ <li>Indians</li>
+ <li>Settlers</li>
+</ol>
+<h4>Heading 4</h4>
+<p>
+ content
+ 4 <a href="http://somewhere.else.com">Somewhere
+ Else</a>
+</p>
+<p>John W. Long wrote:</p>
+<blockquote>
+ <p>Adam Williams wrote:</p>
+ <blockquote>
+ <p>
+ Adam's quote
+ </p>
+ </blockquote>
+ <p>
+ John's quote
+ </p>
+</blockquote>
+<h5>Heading 5</h5>
+<p>
+ content 5
+</p>
+<dl>
+ <dt>Term 1</dt>
+ <dd>
+ def 1
+ </dd>
+ <dt>Term 2</dt>
+ <dd>
+ def
+ 2
+ </dd>
+</dl>
+<h6>Heading 6</h6>
+<p>
+ content 6
+</p>
+</body>
+</html>
View
56 spec/fixtures/html/real_life.txt
@@ -0,0 +1,56 @@
+Heading 1
+
+Lorem ipsum *dolor sit amet*, consectetuer _adipiscing_ elit.
+End of content for heading 1
+
+Heading 2
+
+content 2
+
+ * Apples
+
+ * Oranges
+
+ * Bananas
+
+Heading 3
+
+content 3 Somewhere[1]
+
+ 1. Cowboys
+
+ 2. Indians
+
+ 3. Settlers
+
+Heading 4
+
+content 4 Somewhere Else[2]
+
+John W. Long wrote:
+
+Adam Williams wrote:
+
+Adam's quote
+
+John's quote
+
+Heading 5
+
+content 5
+
+Term 1
+
+def 1
+
+Term 2
+
+def 2
+
+Heading 6
+
+content 6
+
+--
+[1] http://somewhere.com
+[2] http://somewhere.else.com
View
24 spec/models/message_spec.rb
@@ -60,6 +60,30 @@ def message_from_tmail(path)
"This is a simple test."
end
+ it 'should work with multipart/related, no text part' do
+ message_from_tmail('content_types/multipart_related_no_text_plain').text.should == %(I don't really have much to say, so I'm going to share some random things I saw today:
+
+I saw this guy on twitter.com, and he looks pretty chill:
+
+I found this sweet url, and it's not dirty!:
+
+I found out that if I call our Skype phone from Skype on my laptop, my laptop will give me the ability to answer the call I am placing. Freaky!
+
+Here is what my rating star widget looks like:
+
+What's with the dashes and tildes?
+
+Yeah, what is going on with that. They don't even match.
+-~----~~----~----~----~----~---~~-~----~------~--~-~-
+vs
+--~--~---~~----~--~----~-----~~~----~---~---~--~-~--~
+
+
+Good job with this!
+
+-Steve)
+ end
+
it 'should answer text suitable for reply' do
message_from_tmail('content_types/text_plain').text_for_reply.should ==
email_fixture('content_types/text_plain_reply.txt')
View
10 spec/models/util/email_helpers_spec.rb
@@ -30,4 +30,14 @@
remove_regard('Re: Re: Subject').should == 'Subject'
end
end
+
+ describe 'html_to_text' do
+ it 'should handle real life example' do
+ html_to_text(html_fixture('real_life')).should == html_fixture('real_life.txt')
+ end
+
+ it 'should handle lists' do
+ html_to_text('<p>Fruits</p> <ul><li>Apples</li><li>Oranges</li><li>Bananas</li></ul>').should == %{Fruits\n\n * Apples\n\n * Oranges\n\n * Bananas}
+ end
+ end
end
View
8 spec/spec_helper.rb
@@ -46,6 +46,14 @@ def tmail_fixture(path, header_changes = {})
tmail
end
+def html_fixtures_path(path)
+ File.join(SPEC_ROOT, 'fixtures/html', path)
+end
+
+def html_fixture(path)
+ File.read(html_fixtures_path(path))
+end
+
def text_fixtures_path(path)
File.join(SPEC_ROOT, 'fixtures/text', path)
end

0 comments on commit c09111c

Please sign in to comment.
Something went wrong with that request. Please try again.