This repository has been archived by the owner on Jun 22, 2020. It is now read-only.
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 7af928e
Showing
5 changed files
with
659 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
Oops, something went wrong.