Skip to content
This repository has been archived by the owner on Jun 22, 2020. It is now read-only.

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
jystewart committed Jun 28, 2010
0 parents commit 7af928e
Show file tree
Hide file tree
Showing 5 changed files with 659 additions and 0 deletions.
19 changes: 19 additions & 0 deletions README.mdown
@@ -0,0 +1,19 @@
# HTML2Textile #

A quick and simple way to convert HTML to Textile.

`parser = HTMLToTextileParser.new
parser.feed(your_html)
puts parser.to_textile`

## Introduction From 2007 ##

One of the many tricky decisions to be made when building content management tools is how to allow users to control the basic formatting of their input without breaking your carefully crafted layouts or injecting nasty hacks into your pages. One approach has long been to provide your own markup language. Instead of allowing users to write HTML, let them use bbcode, or markdown, or textile, which have more controlled vocabularies and rules that mean it’s much less likely that problems will occur.

Textile in particular has a nice simple syntax and is increasingly popular thanks to its adoption in products like those of 37signals. In Ruby, there’s the RedCloth library which makes it fast and easy to convert textile to HTML. The one problem is if you already have a body of user generated HTML in your legacy system that needs converting. That’s the situation I found myself in this week and I quickly needed a tool to translate the content so that I could get on with the more interesting parts of the system.

Searching for options, the ClothRed library which offers some translation, but it doesn’t handle important elements like links. I considered patching it to handle the elements I need, but in the end I decided to take a different approach and used the SGML parsing library found here to port a python html2textile parser.

Porting code from python to ruby is a pretty straightforward process as the language’s are so similar on a number of levels, but there were several issues to work through, particularly relating to scoping, and quite a few methods to change to make them feel a little more ruby-ish. I’ve not converted all of the entity handling as I didn’t really need it, but there might be a bit of work to do in making sure character set issues are properly taken care of.

The end result is a piece of code that’s now served its purpose and that I’m unlikely to need again for quite a while. It’s not something that I’m particularly proud of, it could almost certainly be implemented more neatly, but I thought I’d throw it out there in case it could be useful to someone else. Should you be inspired to take it and twist it and turn it into a well-heeled, more robust and properly distributable solution, feel free, but please let me know so that at the very least I can update this entry.
35 changes: 35 additions & 0 deletions example.rb
@@ -0,0 +1,35 @@
require 'html2textile'

first_block = <<END
<div class="column span-3">
<h3 class="storytitle entry-title" id="post-312">
<a href="http://jystewart.net/process/2007/11/converting-html-to-textile-with-ruby/" rel="bookmark">Converting HTML to Textile with Ruby</a>
</h3>
<p>
<span>23 November 2007</span>
(<abbr class="updated" title="2007-11-23T19:51:54+00:00">7:51 pm</abbr>)
</p>
<p>
By <span class="author vcard fn">James Stewart</span>
<br />filed under:
<a href="http://jystewart.net/process/category/snippets/" title="View all posts in Snippets" rel="category tag">Snippets</a>
<br />tagged: <a href="http://jystewart.net/process/tag/content-management/" rel="tag">content management</a>,
<a href="http://jystewart.net/process/tag/conversion/" rel="tag">conversion</a>,
<a href="http://jystewart.net/process/tag/html/" rel="tag">html</a>,
<a href="http://jystewart.net/process/tag/python/" rel="tag">Python</a>,
<a href="http://jystewart.net/process/tag/ruby/" rel="tag">ruby</a>,
<a href="http://jystewart.net/process/tag/textile/" rel="tag">textile</a>
</p>
<div class="feedback">
<script src="http://feeds.feedburner.com/~s/jystewart/iLiN?i=http://jystewart.net/process/2007/11/converting-html-to-textile-with-ruby/" type="text/javascript" charset="utf-8"></script>
</div>
</div>
END

parser = HTMLToTextileParser.new
parser.feed(first_block)
puts parser.to_textile
17 changes: 17 additions & 0 deletions html2textile.gemspec
@@ -0,0 +1,17 @@
Gem::Specification.new do |s|
s.platform = Gem::Platform::RUBY
s.name = 'html2textile'
s.version = "1.0.0.beta1"
s.summary = 'Converter from HTML to Textile'
s.description = 'Provides an SGML parser to convert HTML into the Textile format'

s.required_ruby_version = '>= 1.8.7'
s.required_rubygems_version = ">= 1.3.6"

s.author = 'James Stewart'
s.email = 'james@ketlai.co.uk'
s.homepage = 'http://jystewart.net/process/2007/11/converting-html-to-textile-with-ruby'

s.require_path = 'lib'
s.files = Dir.glob("{lib}/**/*") + %w(example.rb README.mdown)
end
255 changes: 255 additions & 0 deletions lib/html2textile.rb
@@ -0,0 +1,255 @@
require 'sgml_parser'

# A class to convert HTML to textile. Based on the python parser
# found at http://aftnn.org/content/code/html2textile/
#
# Read more at http://jystewart.net/process/2007/11/converting-html-to-textile-with-ruby
#
# Author:: James Stewart (mailto:james@ketlai.co.uk)
# Copyright:: Copyright (c) 2010 James Stewart
# License:: Distributes under the same terms as Ruby

# This class is an implementation of an SgmlParser designed to convert
# HTML to textile.
#
# Example usage:
# parser = HTMLToTextileParser.new
# parser.feed(input_html)
# puts parser.to_textile
class HTMLToTextileParser < SgmlParser

attr_accessor :result
attr_accessor :in_block
attr_accessor :data_stack
attr_accessor :a_href
attr_accessor :in_ul
attr_accessor :in_ol

@@permitted_tags = []
@@permitted_attrs = []

def initialize(verbose=nil)
@output = String.new
self.in_block = false
self.result = []
self.data_stack = []
super(verbose)
end

# Normalise space in the same manner as HTML. Any substring of multiple
# whitespace characters will be replaced with a single space char.
def normalise_space(s)
s.to_s.gsub(/\s+/x, ' ')
end

def build_styles_ids_and_classes(attributes)
idclass = ''
idclass += attributes['class'] if attributes.has_key?('class')
idclass += "\##{attributes['id']}" if attributes.has_key?('id')
idclass = "(#{idclass})" if idclass != ''

style = attributes.has_key?('style') ? "{#{attributes['style']}}" : ""
"#{idclass}#{style}"
end

def make_block_start_pair(tag, attributes)
attributes = attrs_to_hash(attributes)
class_style = build_styles_ids_and_classes(attributes)
write("#{tag}#{class_style}. ")
start_capture(tag)
end

def make_block_end_pair
stop_capture_and_write
write("\n\n")
end

def make_quicktag_start_pair(tag, wrapchar, attributes)
attributes = attrs_to_hash(attributes)
class_style = build_styles_ids_and_classes(attributes)
write([" ", "#{wrapchar}#{class_style}"])
start_capture(tag)
end

def make_quicktag_end_pair(wrapchar)
stop_capture_and_write
write([wrapchar, " "])
end

def write(d)
if self.data_stack.size < 2
self.result += d.to_a
else
self.data_stack[-1] += d.to_a
end
end

def start_capture(tag)
self.in_block = tag
self.data_stack.push([])
end

def stop_capture_and_write
self.in_block = false
self.write(self.data_stack.pop)
end

def handle_data(data)
write(normalise_space(data).strip) unless data.nil? or data == ''
end

%w[1 2 3 4 5 6].each do |num|
define_method "start_h#{num}" do |attributes|
make_block_start_pair("h#{num}", attributes)
end

define_method "end_h#{num}" do
make_block_end_pair
end
end

PAIRS = { 'blockquote' => 'bq', 'p' => 'p' }
QUICKTAGS = { 'b' => '*', 'strong' => '*',
'i' => '_', 'em' => '_', 'cite' => '??', 's' => '-',
'sup' => '^', 'sub' => '~', 'code' => '@', 'span' => '%'}

PAIRS.each do |key, value|
define_method "start_#{key}" do |attributes|
make_block_start_pair(value, attributes)
end

define_method "end_#{key}" do
make_block_end_pair
end
end

QUICKTAGS.each do |key, value|
define_method "start_#{key}" do |attributes|
make_quicktag_start_pair(key, value, attributes)
end

define_method "end_#{key}" do
make_quicktag_end_pair(value)
end
end

def start_ol(attrs)
self.in_ol = true
end

def end_ol
self.in_ol = false
write("\n")
end

def start_ul(attrs)
self.in_ul = true
end

def end_ul
self.in_ul = false
write("\n")
end

def start_li(attrs)
if self.in_ol
write("# ")
else
write("* ")
end

start_capture("li")
end

def end_li
stop_capture_and_write
write("\n")
end

def start_a(attrs)
attrs = attrs_to_hash(attrs)
self.a_href = attrs['href']

if self.a_href:
write(" \"")
start_capture("a")
end
end

def end_a
if self.a_href:
stop_capture_and_write
write(["\":", self.a_href, " "])
self.a_href = false
end
end

def attrs_to_hash(array)
array.inject({}) { |collection, part| collection[part[0].downcase] = part[1]; collection }
end

def start_img(attrs)
attrs = attrs_to_hash(attrs)
write([" !", attrs["src"], "! "])
end

def end_img
end

def start_tr(attrs)
end

def end_tr
write("|\n")
end

def start_td(attrs)
write("|")
start_capture("td")
end

def end_td
stop_capture_and_write
write("|")
end

def start_br(attrs)
write("\n")
end

def unknown_starttag(tag, attrs)
if @@permitted_tags.include?(tag)
write(["<", tag])
attrs.each do |key, value|
if @@permitted_attributes.include?(key)
write([" ", key, "=\"", value, "\""])
end
end
end
end

def unknown_endtag(tag)
if @@permitted_tags.include?(tag)
write(["</", tag, ">"])
end
end

# Return the textile after processing
def to_textile
result.join
end

# UNCONVERTED PYTHON METHODS
#
# def handle_charref(self, tag):
# self._write(unichr(int(tag)))
#
# def handle_entityref(self, tag):
# if self.entitydefs.has_key(tag):
# self._write(self.entitydefs[tag])
#
# def handle_starttag(self, tag, method, attrs):
# method(dict(attrs))
#

end

0 comments on commit 7af928e

Please sign in to comment.