├── test ├── fixtures │ ├── author.rb │ ├── story.rb │ ├── authors.yml │ └── stories.yml ├── helper.rb └── textiled_test.rb ├── init.rb ├── about.yml ├── CHANGES ├── Rakefile ├── LICENSE ├── lib └── acts_as_textiled.rb └── README.rdoc /test/fixtures/author.rb: -------------------------------------------------------------------------------- 1 | class Author < ActiveRecord::Base 2 | has_many :stories 3 | acts_as_textiled :blog => :lite_mode 4 | end 5 | -------------------------------------------------------------------------------- /test/fixtures/story.rb: -------------------------------------------------------------------------------- 1 | class Story < ActiveRecord::Base 2 | belongs_to :author 3 | acts_as_textiled :body, :description => :lite_mode 4 | end 5 | -------------------------------------------------------------------------------- /init.rb: -------------------------------------------------------------------------------- 1 | begin 2 | require 'RedCloth' unless defined? RedCloth 3 | rescue LoadError 4 | nil 5 | end 6 | 7 | require 'acts_as_textiled' 8 | ActiveRecord::Base.send(:include, Err::Acts::Textiled) 9 | -------------------------------------------------------------------------------- /about.yml: -------------------------------------------------------------------------------- 1 | author: Chris Wanstrath (chris[at]ozmm[dot]org) 2 | summary: 3 | homepage: http://errtheblog.com/post/14 4 | plugin: http://require.errtheblog.com/svn/acts_as_textiled 5 | license: MIT 6 | version: 0.3 7 | rails_version: 1.1+ 8 | -------------------------------------------------------------------------------- /test/fixtures/authors.yml: -------------------------------------------------------------------------------- 1 | why: 2 | id: 1 3 | name: why the lucky stiff 4 | blog: '"RedHanded":http://redhanded.hobix.com' 5 | defunkt: 6 | id: 2 7 | name: Chris Wanstrath 8 | blog: '"ones zeros majors and minors":http://ozmm.org' 9 | -------------------------------------------------------------------------------- /CHANGES: -------------------------------------------------------------------------------- 1 | = 0.3 2 | - Fixed Model.textiled = false bug 3 | - Refactored tests 4 | - Changed api from @story.description_plain to @story.description(:plain) - kept old way, though 5 | 6 | = 0.2 7 | 8 | * Fix issue with object.attribute_plain overwriting the original attribute [Thanks, James] 9 | * Fix issue with attributes trying to work on nil values [Thanks again, James] 10 | 11 | = 0.1 12 | 13 | * Initial import 14 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake' 2 | require 'rake/testtask' 3 | require 'rake/rdoctask' 4 | 5 | desc 'Generate RDoc documentation for the acts_as_textiled plugin.' 6 | Rake::RDocTask.new(:rdoc) do |rdoc| 7 | files = ['README', 'LICENSE', 'lib/**/*.rb'] 8 | rdoc.rdoc_files.add(files) 9 | rdoc.main = "README" # page to start on 10 | rdoc.title = "acts_as_textiled" 11 | rdoc.template = File.exists?(t="/Users/chris/ruby/projects/err/rock/template.rb") ? t : "/var/www/rock/template.rb" 12 | rdoc.rdoc_dir = 'doc' # rdoc output folder 13 | rdoc.options << '--inline-source' 14 | end 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2006 Chris Wanstrath 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /test/fixtures/stories.yml: -------------------------------------------------------------------------------- 1 | sandbox: 2 | id: 1 3 | author_id: 1 4 | title: The Thrilling Freaky-Freaky Sandbox Hack 5 | body: Holy cats, I'm proud to offer you this sensational hack today. For me, this is monumental, as it culminates a number of sundry microhacks from the past few years and gets us a step closer to realizing Try Ruby out in the broader kingdoms. This is the sort of thing that will make you want to post spangly angel GIFs in the comments. 6 | description: _why announces __Sandbox__ 7 | irb: 8 | id: 2 9 | author_id: 2 10 | title: Simpler IRB 11 | description: __Beautify__ your *IRb* prompt 12 | body: | 13 | Sick of that ugly irb prompt? Too much information. 14 | 15 | $ irb 16 | irb(main):001:0> "i dont care how deeply nested i yam".nil? 17 | => false 18 | Check it. Stick this in your .irbrc: 19 | 20 | IRB.conf[:PROMPT_MODE] = :SIMPLE 21 | Now we get a prompt which is, well, simple: 22 | 23 | $ irb 24 | >> !!nil 25 | => false 26 | Less noise. 27 | textile: 28 | id: 3 29 | author_id: 2 30 | title: I am a fan of Textile. 31 | description: Chris explains why Textile is useful. 32 | body: | 33 | _Textile_ is useful because it makes text _slightly_ easier to *read*. 34 | 35 | If only it were so *easy* to use in every programming language. In Rails, 36 | with the help of "acts_as_textiled":http://google.com/search?q=acts_as_textiled, 37 | it's way easy. Thanks in no small part to %{color:red}RedCloth%, of course. 38 | legalize: 39 | id: 4 40 | author_id: 2 41 | title: This is a bunch of text about things. 42 | body: | 43 | Is Textile(TM) the wave of the future? What about acts_as_textiled(C)? It's 44 | doubtful. Why does Textile(TM) smell like _Python_? Can we do anything to 45 | fix that? No? Well, I guess there are worse smells - like Ruby. jk. 46 | 47 | But seriously, ice > water and water < rain. But...nevermind. 1 x 1? 1. 48 | 49 | "You're a good kid," he said. "Keep it up." 50 | -------------------------------------------------------------------------------- /test/helper.rb: -------------------------------------------------------------------------------- 1 | $:.unshift File.dirname(__FILE__) + '/../lib' 2 | 3 | begin 4 | require 'rubygems' 5 | require 'yaml' 6 | require 'mocha' 7 | require 'active_support' 8 | require 'test/spec' 9 | require 'RedCloth' 10 | rescue LoadError 11 | puts "acts_as_textiled requires the mocha and test-spec gems to run its tests" 12 | exit 13 | end 14 | 15 | class ActiveRecord 16 | class Base 17 | attr_reader :attributes 18 | 19 | def initialize(attributes = {}) 20 | @attributes = attributes.dup 21 | after_find if respond_to?(:after_find) 22 | end 23 | 24 | def method_missing(name, *args) 25 | if name.to_s[/=/] 26 | @attributes[key = name.to_s.sub('=','')] = value = args.first 27 | write_attribute key, value 28 | else 29 | self[name.to_s] 30 | end 31 | end 32 | 33 | def save 34 | true 35 | end 36 | 37 | def reload 38 | self 39 | end 40 | 41 | def [](value) 42 | @attributes[value.to_s.sub('_before_type_cast', '')] 43 | end 44 | 45 | def self.global 46 | eval("$#{name.downcase}") 47 | end 48 | 49 | def self.find(id) 50 | item = global.detect { |key, hash| hash['id'] == id }.last 51 | new(item) 52 | end 53 | end 54 | end unless defined? ActiveRecord 55 | 56 | require File.dirname(__FILE__) + '/../init' 57 | 58 | class Author < ActiveRecord::Base 59 | acts_as_textiled :blog => :lite_mode 60 | end 61 | 62 | class Story < ActiveRecord::Base 63 | acts_as_textiled :body, :description => :lite_mode 64 | 65 | def author 66 | @author ||= Author.find(author_id) 67 | end 68 | end 69 | 70 | class StoryWithAfterFind < Story 71 | acts_as_textiled :body, :description => :lite_mode 72 | 73 | def after_find 74 | textilize 75 | end 76 | 77 | def self.name 78 | Story.name 79 | end 80 | 81 | def author 82 | @author ||= Author.find(author_id) 83 | end 84 | end 85 | 86 | $author = YAML.load_file(File.dirname(__FILE__) + '/fixtures/authors.yml') 87 | $story = YAML.load_file(File.dirname(__FILE__) + '/fixtures/stories.yml') 88 | -------------------------------------------------------------------------------- /lib/acts_as_textiled.rb: -------------------------------------------------------------------------------- 1 | module Err 2 | module Acts #:nodoc: all 3 | module Textiled 4 | def self.included(klass) 5 | klass.extend ClassMethods 6 | end 7 | 8 | module ClassMethods 9 | def acts_as_textiled(*attributes) 10 | @textiled_attributes = [] 11 | 12 | @textiled_unicode = String.new.respond_to? :chars 13 | 14 | ruled = attributes.last.is_a?(Hash) ? attributes.pop : {} 15 | attributes += ruled.keys 16 | 17 | type_options = %w( plain source ) 18 | 19 | attributes.each do |attribute| 20 | define_method(attribute) do |*type| 21 | type = type.first 22 | 23 | if type.nil? && self[attribute] 24 | textiled[attribute.to_s] ||= RedCloth.new(self[attribute], Array(ruled[attribute])).to_html 25 | elsif type.nil? && self[attribute].nil? 26 | nil 27 | elsif type_options.include?(type.to_s) 28 | send("#{attribute}_#{type}") 29 | else 30 | raise "I don't understand the `#{type}' option. Try #{type_options.join(' or ')}." 31 | end 32 | end 33 | 34 | define_method("#{attribute}_plain", proc { strip_redcloth_html(__send__(attribute)) if __send__(attribute) } ) 35 | define_method("#{attribute}_source", proc { __send__("#{attribute}_before_type_cast") } ) 36 | 37 | @textiled_attributes << attribute 38 | end 39 | 40 | include Err::Acts::Textiled::InstanceMethods 41 | end 42 | 43 | def textiled_attributes 44 | Array(@textiled_attributes) 45 | end 46 | end 47 | 48 | module InstanceMethods 49 | def textiled 50 | textiled? ? (@textiled ||= {}) : @attributes.dup 51 | end 52 | 53 | def textiled? 54 | @is_textiled != false 55 | end 56 | 57 | def textiled=(bool) 58 | @is_textiled = !!bool 59 | end 60 | 61 | def textilize 62 | self.class.textiled_attributes.each { |attr| __send__(attr) } 63 | end 64 | 65 | def reload 66 | textiled.clear 67 | super 68 | end 69 | 70 | def write_attribute(attr_name, value) 71 | textiled[attr_name.to_s] = nil 72 | super 73 | end 74 | 75 | private 76 | def strip_redcloth_html(html) 77 | returning html.dup.gsub(html_regexp, '') do |h| 78 | redcloth_glyphs.each do |(entity, char)| 79 | sub = [ :gsub!, entity, char ] 80 | @textiled_unicode ? h.chars.send(*sub) : h.send(*sub) 81 | end 82 | end 83 | end 84 | 85 | def redcloth_glyphs 86 | [[ '’', "'" ], 87 | [ '‘', "'" ], 88 | [ '<', '<' ], 89 | [ '>', '>' ], 90 | [ '”', '"' ], 91 | [ '“', '"' ], 92 | [ '…', '...' ], 93 | [ '\1—', '--' ], 94 | [ ' → ', '->' ], 95 | [ ' – ', '-' ], 96 | [ '×', 'x' ], 97 | [ '™', '(TM)' ], 98 | [ '®', '(R)' ], 99 | [ '©', '(C)' ]] 100 | end 101 | 102 | def html_regexp 103 | %r{<(?:[^>"']+|"(?:\\.|[^\\"]+)*"|'(?:\\.|[^\\']+)*')*>}xm 104 | end 105 | end 106 | end 107 | end 108 | end 109 | -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | = Acts as Textiled 2 | 3 | This simple plugin allows you to forget about constantly rendering Textile in 4 | your application. Instead, you can rest easy knowing the Textile fields you 5 | want to display as HTML will always be displayed as HTML (unless you tell your 6 | code otherwise). 7 | 8 | No database modifications are needed. 9 | 10 | You need RedCloth, of course. And Rails. 11 | 12 | == Usage 13 | 14 | class Story < ActiveRecord::Base 15 | acts_as_textiled :body_text, :description 16 | end 17 | 18 | >> story = Story.find(3) 19 | => # 20 | 21 | >> story.description 22 | => "

This is cool.

" 23 | 24 | >> story.description(:source) 25 | => "This is *cool*." 26 | 27 | >> story.description(:plain) 28 | => "This is cool." 29 | 30 | >> story.description = "I _know_!" 31 | => "I _know_!" 32 | 33 | >> story.save 34 | => true 35 | 36 | >> story.description 37 | => "

I know!

" 38 | 39 | >> story.textiled = false 40 | => false 41 | 42 | >> story.description 43 | => "I _know_!" 44 | 45 | >> story.textiled = true 46 | => true 47 | 48 | >> story.description 49 | => "

I know!

" 50 | 51 | == Different Modes 52 | 53 | RedCloth supports different modes, such as :lite_mode. To use a mode on 54 | a specific attribute simply pass it in as an options hash after any 55 | attributes you don't want to mode-ify. Like so: 56 | 57 | class Story < ActiveRecord::Base 58 | acts_as_textiled :body_text, :description => :lite_mode 59 | end 60 | 61 | Or: 62 | 63 | class Story < ActiveRecord::Base 64 | acts_as_textiled :body_text => :lite_mode, :description => :lite_mode 65 | end 66 | 67 | You can also pass in multiple modes per attribute: 68 | 69 | class Story < ActiveRecord::Base 70 | acts_as_textiled :body_text, :description => [ :lite_mode, :no_span_caps ] 71 | end 72 | 73 | Get it? Now let's say you have an admin tool and you want the text to be displayed 74 | in the text boxes / fields as plaintext. Do you have to change all your views? 75 | 76 | Hell no. 77 | 78 | == form_for 79 | 80 | Are you using form_for? If you are, you don't have to change any code at all. 81 | 82 | <% form_for :story, @story do |f| %> 83 | Description:
<%= f.text_field :description %> 84 | <% end %> 85 | 86 | You'll see the Textile plaintext in the text field. It Just Works. 87 | 88 | == form tags 89 | 90 | If you're being a bit unconvential, no worries. You can still get at your 91 | raw Textile like so: 92 | 93 | Description:
<%= text_field_tag :description, @story.description(:source) %> 94 | 95 | And there's always object.textiled = false, as demo'd above. 96 | 97 | == Pre-fetching 98 | 99 | acts_as_textiled locally caches rendered HTML once the attribute in question has 100 | been requested. Obviously this doesn't bode well for marshalling or caching. 101 | 102 | If you need to force your object to build and cache HTML for all textiled attributes, 103 | call the +textilize+ method on your object. 104 | 105 | If you're real crazy you can even do something like this: 106 | 107 | class Story < ActiveRecord::Base 108 | acts_as_textiled :body_text, :description 109 | 110 | def after_find 111 | textilize 112 | end 113 | end 114 | 115 | All your Textile will now be ready to go in spiffy HTML format. But you probably 116 | won't need to do this. 117 | 118 | Enjoy. 119 | 120 | * By Chris Wanstrath [ chris[at]ozmm[dot]org ] 121 | -------------------------------------------------------------------------------- /test/textiled_test.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), 'helper') 2 | 3 | context "An ActiveRecord instance acting as textiled" do 4 | specify "should return nil for empty fields" do 5 | story = Story.new 6 | 7 | story.description.should.be.nil 8 | story.description_source.should.be.nil 9 | story.description_plain.should.be.nil 10 | end 11 | 12 | specify "should enhance attributes with html, textile, and plain versions" do 13 | story = Story.find(1) 14 | 15 | desc_html = '_why announces Sandbox' 16 | desc_textile = '_why announces __Sandbox__' 17 | desc_plain = '_why announces Sandbox' 18 | 19 | story.description.should.equal desc_html 20 | story.description(:source).should.equal desc_textile 21 | story.description(:plain).should.equal desc_plain 22 | 23 | story.description_source.should.equal desc_textile 24 | story.description_plain.should.equal desc_plain 25 | 26 | # make sure we don't overwrite anything - thanks James 27 | story.description.should.equal desc_html 28 | story.description(:source).should.equal desc_textile 29 | story.description(:plain).should.equal desc_plain 30 | 31 | story.description_source.should.equal desc_textile 32 | story.description_plain.should.equal desc_plain 33 | end 34 | 35 | specify "should raise when given a non-sensical option" do 36 | story = Story.find(1) 37 | 38 | proc { story.description(:cassadaga) }.should.raise 39 | end 40 | 41 | specify "should pick up changes to attributes" do 42 | story = Story.find(2) 43 | 44 | start_html = 'Beautify your IRb prompt' 45 | story.description.should.equal start_html 46 | 47 | story.description = "**IRb** is simple" 48 | changed_html = "IRb is simple" 49 | story.description.should.equal changed_html 50 | 51 | story.save 52 | 53 | story.description.should.equal changed_html 54 | story.description_plain.should.equal 'IRb is simple' 55 | end 56 | 57 | specify "should be able to toggle whether textile is active or not" do 58 | story = Story.find(2) 59 | 60 | desc_html = 'Beautify your IRb prompt' 61 | desc_textile = '__Beautify__ your *IRb* prompt' 62 | 63 | story.description.should.equal desc_html 64 | story.textiled = false 65 | story.description.should.equal desc_textile 66 | 67 | story.save 68 | 69 | story.description.should.equal desc_textile 70 | story.textiled = true 71 | story.description.should.equal desc_html 72 | end 73 | 74 | specify "should textile attributes across associations" do 75 | story = Story.find(2) 76 | 77 | blog_html = 'ones zeros majors and minors' 78 | blog_textile = '"ones zeros majors and minors":http://ozmm.org' 79 | blog_plain = 'ones zeros majors and minors' 80 | 81 | story.author.blog.should.equal blog_html 82 | story.author.blog_source.should.equal blog_textile 83 | story.author.blog_plain.should.equal blog_plain 84 | end 85 | 86 | specify "should be able to toggle across associations" do 87 | story = Story.find(1) 88 | 89 | blog_html = 'RedHanded' 90 | blog_textile = '"RedHanded":http://redhanded.hobix.com' 91 | blog_plain = 'RedHanded' 92 | 93 | story.author.blog.should.equal blog_html 94 | story.author.textiled = false 95 | 96 | story.author.blog.should.equal blog_textile 97 | story.author.textiled = true 98 | 99 | story.author.blog.should.equal blog_html 100 | end 101 | 102 | specify "should enhance text attributes" do 103 | story = Story.find(3) 104 | 105 | body_html = %[

Textile is useful because it makes text slightly easier to read.

\n\n\n\t

If only it were so easy to use in every programming language. In Rails,\nwith the help of acts_as_textiled,\nit’s way easy. Thanks in no small part to RedCloth, of course.

] 106 | body_textile = %[_Textile_ is useful because it makes text _slightly_ easier to *read*.\n\nIf only it were so *easy* to use in every programming language. In Rails,\nwith the help of "acts_as_textiled":http://google.com/search?q=acts_as_textiled,\nit's way easy. Thanks in no small part to %{color:red}RedCloth%, of course.\n] 107 | body_plain = %[Textile is useful because it makes text slightly easier to read.\n\n\n\tIf only it were so easy to use in every programming language. In Rails,\nwith the help of acts_as_textiled,\nit's way easy. Thanks in no small part to RedCloth, of course.] 108 | 109 | story.body.should.equal body_html 110 | story.body_source.should.equal body_textile 111 | story.body_plain.should.equal body_plain 112 | end 113 | 114 | specify "should handle character conversions" do 115 | story = Story.find(4) 116 | 117 | body_html = "

Is Textile™ the wave of the future? What about acts_as_textiled©? It’s\ndoubtful. Why does Textile™ smell like Python? Can we do anything to\nfix that? No? Well, I guess there are worse smells – like Ruby. jk.

\n\n\n\t

But seriously, ice > water and water < rain. But…nevermind. 1×1? 1.

\n\n\n\t

“You’re a good kid,” he said. “Keep it up.”

" 118 | body_plain = %[Is Textile(TM) the wave of the future? What about acts_as_textiled(C)? It's\ndoubtful. Why does Textile(TM) smell like Python? Can we do anything to\nfix that? No? Well, I guess there are worse smells-like Ruby. jk.\n\n\n\tBut seriously, ice > water and water < rain. But...nevermind. 1x1? 1.\n\n\n\t"You're a good kid," he said. "Keep it up."] 119 | 120 | story.body.should.equal body_html 121 | story.body_plain.should.equal body_plain 122 | end 123 | 124 | specify "should be able to do on-demand textile caching" do 125 | story = Story.find(1) 126 | 127 | desc_html = '_why announces Sandbox' 128 | 129 | story.textiled.size.should.equal 0 130 | 131 | story.textilize 132 | 133 | story.textiled.size.should.equal 2 134 | story.description.should.equal desc_html 135 | end 136 | 137 | specify "should work well with after_find callbacks" do 138 | story = StoryWithAfterFind.find(2) 139 | 140 | desc_html = 'Beautify your IRb prompt' 141 | 142 | story.textiled.size.should.equal 2 143 | story.description.should.equal desc_html 144 | end 145 | end 146 | --------------------------------------------------------------------------------