Permalink
Browse files

Implemented Typogrify

  • Loading branch information...
1 parent 5a48494 commit bfa71fda1c32eacc9a4d12a451fdeb053727992f @avdgaag committed Jul 26, 2010
Showing with 316 additions and 22 deletions.
  1. +2 −0 .gitignore
  2. +1 −1 LICENSE
  3. +67 −0 README.md
  4. +0 −17 README.rdoc
  5. +2 −1 Rakefile
  6. +173 −0 lib/typogrify.rb
  7. +1 −0 test/helper.rb
  8. +70 −3 test/test_typogrify.rb
View
@@ -19,3 +19,5 @@ rdoc
pkg
## PROJECT::SPECIFIC
+.yardoc
+doc
View
@@ -1,4 +1,4 @@
-Copyright (c) 2009 Arjan van der Gaag
+Copyright (c) 2010 Arjan van der Gaag
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
View
@@ -0,0 +1,67 @@
+# typogrify
+
+Helps you improve your web typograpbhy with some standard text filters.
+
+This project is based on Django's Typogrify, so the best introduction to read would be [Jeff Croft's][1].
+
+I created this gem to easily share these text filters in some tiny Ruby projects, including a TextMate bundle. For production code I recommend checking out the originals first.
+
+## General Usage
+
+First, install the Ruby gem:
+
+ $ gem install typogrify
+
+Then require the library to get started:
+
+ require 'typogrify'
+ Typogrify.improve('Hello, world!')
+
+Or, you can include the library in a helper or something:
+
+ require 'typogrify'
+ include Typogrify
+ improve('Hello, world!')
+
+## References
+
+* Based on [typography-helper][2]
+* ...and on [Typogrify][3]
+* [Description of typogrify][1]
+
+[1]: http://jeffcroft.com/blog/2007/may/29/typogrify-easily-produce-web-typography-doesnt-suc/
+[2]: http://github.com/hunter/typography-helper
+[3]: http://code.google.com/p/typogrify
+
+## Note on Patches/Pull Requests
+
+* Fork the project.
+* Make your feature addition or bug fix.
+* Add tests for it. This is important so I don't break it in a
+ future version unintentionally.
+* Commit, do not mess with rakefile, version, or history.
+ (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
+* Send me a pull request. Bonus points for topic branches.
+
+## License
+
+Copyright (c) 2010 Arjan van der Gaag
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
View
@@ -1,17 +0,0 @@
-= typogrify
-
-Description goes here.
-
-== Note on Patches/Pull Requests
-
-* Fork the project.
-* Make your feature addition or bug fix.
-* Add tests for it. This is important so I don't break it in a
- future version unintentionally.
-* Commit, do not mess with rakefile, version, or history.
- (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
-* Send me a pull request. Bonus points for topic branches.
-
-== Copyright
-
-Copyright (c) 2010 Arjan van der Gaag. See LICENSE for details.
View
@@ -6,11 +6,12 @@ begin
Jeweler::Tasks.new do |gem|
gem.name = "typogrify"
gem.summary = %Q{Improves web typography like Django's Typogrify}
- gem.description = %Q{TODO: longer description of your gem}
+ gem.description = %Q{Improve web typography using various text filters. This gem prevents widows and applies markup to ampersans, consecutive capitals and initial quotes.}
gem.email = "arjan@arjanvandergaag.nl"
gem.homepage = "http://github.com/avdgaag/typogrify"
gem.authors = ["Arjan van der Gaag"]
gem.add_development_dependency "yard", ">= 0"
+ gem.add_dependency "rubypants", ">= 0"
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
end
Jeweler::GemcutterTasks.new
View
@@ -0,0 +1,173 @@
+require 'rubypants'
+
+# A collection of simple helpers for improving web
+# typograhy. Based on TypographyHelper by Luke Hartman and Typogrify.
+#
+# @see http://github.com/hunter/typography-helper
+# @see http://code.google.com/p/typogrify
+# @author Arjan van der Gaag <arjan.vandergaag@gmail.com>
+module Typogrify
+
+ # Get the current gem version number
+ # @return [String]
+ def version
+ File.read(File.join(File.dirname(__FILE__), *%w{.. VERSION}))
+ end
+
+ # Applies smartypants to a given piece of text
+ #
+ # @example
+ # smartypants('The "Green" man')
+ # # => 'The &#8220;Green&#8221; man'
+ #
+ # @param [String] text input text
+ # @return [String] input text with smartypants applied
+ def smartypants(text)
+ ::RubyPants.new(text).to_html
+ end
+
+ # converts a & surrounded by optional whitespace or a non-breaking space
+ # to the HTML entity and surrounds it in a span with a styled class
+ #
+ # @example
+ # amp('One & two')
+ # # => 'One <span class="amp">&amp;</span> two'
+ # amp('One &amp; two')
+ # # => 'One <span class="amp">&amp;</span> two'
+ # amp('One &#38; two')
+ # # => 'One <span class="amp">&amp;</span> two'
+ # amp('One&nbsp;&amp;&nbsp;two')
+ # # => 'One&nbsp;<span class="amp">&amp;</span>&nbsp;two'
+ #
+ # @example It won't mess up & that are already wrapped, in entities or URLs
+ #
+ # amp('One <span class="amp">&amp;</span> two')
+ # # => 'One <span class="amp">&amp;</span> two'
+ # amp('&ldquo;this&rdquo; & <a href="/?that&amp;test">that</a>')
+ # # => '&ldquo;this&rdquo; <span class="amp">&amp;</span> <a href="/?that&amp;test">that</a>'
+ #
+ # @example It should ignore standalone amps that are in attributes
+ # amp('<link href="xyz.html" title="One & Two">xyz</link>')
+ # # => '<link href="xyz.html" title="One & Two">xyz</link>'
+ #
+ # @param [String] text input text
+ # @return [String] input text with ampersands wrapped
+ def amp(text)
+ # $1 is an excluded HTML tag, $2 is the part before the caps and $3 is the amp match
+ text.gsub(/<(code|pre).+?<\/\1>|(\s|&nbsp;)&(?:amp;|#38;)?(\s|&nbsp;)/) {|str|
+ $1 ? str : $2 + '<span class="amp">&amp;</span>' + $3 }.gsub(/(\w+)="(.*?)<span class="amp">&amp;<\/span>(.*?)"/, '\1="\2&amp;\3"')
+ end
+
+ # replaces space(s) before the last word (or tag before the last word)
+ # before an optional closing element (<tt>a</tt>, <tt>em</tt>,
+ # <tt>span</tt>, strong) before a closing tag (<tt>p</tt>, <tt>h[1-6]</tt>,
+ # <tt>li</tt>, <tt>dt</tt>, <tt>dd</tt>) or the end of the string
+ #
+ # @example
+ # > widont('A very simple test')
+ # # => 'A very simple&nbsp;test'
+ #
+ # @example Single word items shouldn't be changed
+ # widont('Test')
+ # # => 'Test'
+ # widont(' Test')
+ # # => ' Test'
+ # widont('<ul><li>Test</p></li><ul>')
+ # # => '<ul><li>Test</p></li><ul>'
+ # widont('<ul><li> Test</p></li><ul>')
+ # # => '<ul><li> Test</p></li><ul>'
+ #
+ # @example Nested tags
+ # widont('<p>In a couple of paragraphs</p><p>paragraph two</p>')
+ # # => '<p>In a couple of&nbsp;paragraphs</p><p>paragraph&nbsp;two</p>'
+ # widont('<h1><a href="#">In a link inside a heading</i> </a></h1>')
+ # # => '<h1><a href="#">In a link inside a&nbsp;heading</i> </a></h1>'
+ # widont('<h1><a href="#">In a link</a> followed by other text</h1>')
+ # # => '<h1><a href="#">In a link</a> followed by other&nbsp;text</h1>'
+ #
+ # @example Empty HTMLs shouldn't error
+ # widont('<h1><a href="#"></a></h1>')
+ # # => '<h1><a href="#"></a></h1>'
+ #
+ # @example Excluded tags
+ # widont('<div>Divs get no love!</div>')
+ # # => '<div>Divs get no love!</div>'
+ # widont('<pre>Neither do PREs</pre>')
+ # # => '<pre>Neither do PREs</pre>'
+ # widont('<div><p>But divs with paragraphs do!</p></div>')
+ # # => '<div><p>But divs with paragraphs&nbsp;do!</p></div>'
+ #
+ # @see http://mucur.name/posts/widon-t-and-smartypants-helpers-for-rails
+ # @see http://shauninman.com/archive/2006/08/22/widont_wordpress_plugin
+ # @param [String] text input text
+ # @return [String] input text with non-breaking spaces inserted
+ def widont(text)
+ text.gsub(%r{
+ ((?:</?(?:a|em|span|strong|i|b)[^>]*>)|[^<>\s]) # must be proceeded by an approved inline opening or closing tag or a nontag/nonspace
+ \s+ # the space to replace
+ ([^<>\s]+ # must be flollowed by non-tag non-space characters
+ \s* # optional white space!
+ (</(a|em|span|strong|i|b)>\s*)* # optional closing inline tags with optional white space after each
+ ((</(p|h[1-6]|li|dt|dd)>)|$)) # end with a closing p, h1-6, li or the end of the string
+ }x, '\1&nbsp;\2')
+ end
+
+ # surrounds two or more consecutive captial letters, perhaps with interspersed digits and periods
+ # in a span with a styled class
+ #
+ # @example
+ # caps("A message from KU")
+ # # => 'A message from <span class="caps">KU</span>'
+ #
+ # @example Allows digits
+ # caps("A message from 2KU2 with digits")
+ # # => 'A message from <span class="caps">2KU2</span> with digits'
+ #
+ # @example All caps with with apostrophes in them shouldn't break. Only handles dump apostrophes though.
+ # caps("JIMMY'S")
+ # # => '<span class="caps">JIMMY\\'S</span>'
+ # caps("<i>D.O.T.</i>HE34T<b>RFID</b>")
+ # # => '<i><span class="caps">D.O.T.</span></i><span class="caps">HE34T</span><b><span class="caps">RFID</span></b>'
+ #
+ # @param [String] text input text
+ # @return [String] input text with caps wrapped
+ def caps(text)
+ # $1 is an excluded HTML tag, $2 is the part before the caps and $3 is the caps match
+ text.gsub(/<(code|pre).+?<\/\1>|(\s|&nbsp;|^|'|"|>)([A-Z\d][A-Z\d\.']{1,})(?!\w)/) {|str|
+ $1 ? str : $2 + '<span class="caps">' + $3 + '</span>' }
+ end
+
+ # encloses initial single or double quote, or their entities
+ # (optionally preceeded by a block element and perhaps an inline element)
+ # with a span that can be styled
+ #
+ # @example
+ # initial_quotes('"With primes"')
+ # # => '<span class="dquo">"</span>With primes"'
+ # initial_quotes("'With single primes'")
+ # # => '<span class="quo">\\'</span>With single primes\\''
+ #
+ # @example With primes and links
+ # initial_quotes('<a href="#">"With primes and a link"</a>')
+ # # => '<a href="#"><span class="dquo">"</span>With primes and a link"</a>'
+ #
+ # @example with Smartypants-quotes
+ # initial_quotes('&#8220;With smartypanted quotes&#8221;')
+ # # => '<span class="dquo">&#8220;</span>With smartypanted quotes&#8221;'
+ #
+ # @param [String] text input text
+ # @return [String] input text with initial quotes wrapped
+ def initial_quotes(text)
+ # $1 is the initial part of the string, $2 is the quote or entitity, and $3 is the double quote
+ text.gsub(/((?:<(?:h[1-6]|p|li|dt|dd)[^>]*>|^)\s*(?:<(?:a|em|strong|span)[^>]*>)?)('|&#8216;|("|&#8220;))/) {$1 + "<span class=\"#{'d' if $3}quo\">#{$2}</span>"}
+ end
+
+ # main function to do all the functions from the method
+ # @param [String] text input text
+ # @return [String] input text with all filters applied
+ def improve(text)
+ initial_quotes(caps(smartypants(widont(amp(text)))))
+ end
+
+ extend self
+end
View
@@ -6,4 +6,5 @@
require 'typogrify'
class Test::Unit::TestCase
+ include Typogrify
end
View
@@ -1,7 +1,74 @@
require 'helper'
class TestTypogrify < Test::Unit::TestCase
- def test_something_for_real
- flunk "hey buddy, you should probably rename this file and start testing for real"
+
+ def test_should_replace_amps
+ assert_equal 'One <span class="amp">&amp;</span> two', amp('One & two')
+ assert_equal 'One <span class="amp">&amp;</span> two', amp('One &amp; two')
+ assert_equal 'One <span class="amp">&amp;</span> two', amp('One &#38; two')
+ assert_equal 'One&nbsp;<span class="amp">&amp;</span>&nbsp;two', amp('One&nbsp;&amp;&nbsp;two')
+ end
+
+ def test_should_ignore_special_amps
+ assert_equal 'One <span class="amp">&amp;</span> two', amp('One <span class="amp">&amp;</span> two')
+ assert_equal '&ldquo;this&rdquo; <span class="amp">&amp;</span> <a href="/?that&amp;test">that</a>', amp('&ldquo;this&rdquo; & <a href="/?that&amp;test">that</a>')
+ end
+
+ def test_should_ignore_standalone_amps_in_attributes
+ assert_equal '<link href="xyz.html" title="One &amp; Two">xyz</link>', amp('<link href="xyz.html" title="One & Two">xyz</link>')
+ end
+
+ def test_should_replace_caps
+ assert_equal 'A message from <span class="caps">KU</span>', caps("A message from KU")
+ end
+
+ def test_should_ignore_special_case_caps
+ assert_equal '<pre>CAPS</pre> more <span class="caps">CAPS</span>', caps("<pre>CAPS</pre> more CAPS")
+ assert_equal 'A message from <span class="caps">2KU2</span> with digits', caps("A message from 2KU2 with digits")
+ assert_equal 'Dotted caps followed by spaces should never include them in the wrap <span class="caps">D.O.T.</span> like so.', caps("Dotted caps followed by spaces should never include them in the wrap D.O.T. like so.")
+ end
+
+ def test_should_not_break_caps_with_apostrophes
+ assert_equal '<span class="caps">JIMMY\'S</span>', caps("JIMMY'S")
+ assert_equal '<i><span class="caps">D.O.T.</span></i><span class="caps">HE34T</span><b><span class="caps">RFID</span></b>', caps("<i>D.O.T.</i>HE34T<b>RFID</b>")
+ end
+
+ def test_should_replace_quotes
+ assert_equal '<span class="dquo">"</span>With primes"', initial_quotes('"With primes"')
+ assert_equal '<span class="quo">\'</span>With single primes\'', initial_quotes("'With single primes'")
+ assert_equal '<a href="#"><span class="dquo">"</span>With primes and a link"</a>', initial_quotes('<a href="#">"With primes and a link"</a>')
+ assert_equal '<span class="dquo">&#8220;</span>With smartypanted quotes&#8221;', initial_quotes('&#8220;With smartypanted quotes&#8221;')
+ end
+
+ def test_should_apply_smartypants
+ assert_equal 'The &#8220;Green&#8221; man', smartypants('The "Green" man')
+ end
+
+ def test_should_apply_all_filters
+ assert_equal '<h2><span class="dquo">&#8220;</span>Jayhawks&#8221; <span class="amp">&amp;</span> <span class="caps">KU</span> fans act extremely&nbsp;obnoxiously</h2>', improve('<h2>"Jayhawks" & KU fans act extremely obnoxiously</h2>')
+ end
+
+ def test_should_prevent_widows
+ assert_equal 'A very simple&nbsp;test', widont('A very simple test')
+ end
+
+ def test_should_not_change_single_word_items
+ assert_equal 'Test', widont('Test')
+ assert_equal ' Test', widont(' Test')
+ assert_equal '<ul><li>Test</p></li><ul>', widont('<ul><li>Test</p></li><ul>')
+ assert_equal '<ul><li> Test</p></li><ul>', widont('<ul><li> Test</p></li><ul>')
+ assert_equal '<p>In a couple of&nbsp;paragraphs</p><p>paragraph&nbsp;two</p>', widont('<p>In a couple of paragraphs</p><p>paragraph two</p>')
+ assert_equal '<h1><a href="#">In a link inside a&nbsp;heading</i> </a></h1>', widont('<h1><a href="#">In a link inside a heading</i> </a></h1>')
+ assert_equal '<h1><a href="#">In a link</a> followed by other&nbsp;text</h1>', widont('<h1><a href="#">In a link</a> followed by other text</h1>')
+ end
+
+ def test_should_not_error_on_empty_html
+ assert_equal '<h1><a href="#"></a></h1>', widont('<h1><a href="#"></a></h1>')
+ end
+
+ def test_should_ignore_widows_in_special_tags
+ assert_equal '<div>Divs get no love!</div>', widont('<div>Divs get no love!</div>')
+ assert_equal '<pre>Neither do PREs</pre>', widont('<pre>Neither do PREs</pre>')
+ assert_equal '<div><p>But divs with paragraphs&nbsp;do!</p></div>', widont('<div><p>But divs with paragraphs do!</p></div>')
end
-end
+end

0 comments on commit bfa71fd

Please sign in to comment.