html_slicer

A little gem for Rails 3 helps you implement a smart way to split textual content. This is a quick approach to create ‘pageable’ views for ActiveRecord Model’s attributes, just independent strings, or any other Ruby classes. Or course it can split HTML content. More over, it can ‘resize’ HTML tags through width= attribute*.

* Imagine you want to resize <iframe> embeddings from YouTube saved in static content.

Features

Clean

Does not globally pollute Array, Hash, Object or AR::Base.

Easy to use

Bundle the gem, then your models are ready to implement slicing.

Flexible configuration

Besides global configuration (default), you can create stylized app-level configurations; also any configuration can be tuned for every single implementation.

Flexible options

You can split your text with any Regexp clause: set up which node from your HTML content you want to split, or which you don’t want to. You can also use processor(s) to transform the content before it has to be sliced. Param name, used as a slice number key accessor, can be nested in another param key (nested params). Resizing HTML tags having width= attribute is a helpful feature too.

ORM & template engine agnostic

Supports Rails 3 and multiple template engines (ERB, Haml).

Modern

The slicer helper outputs the HTML5 <nav> tag by default. In addition, the helper supports Rails 3 unobtrusive Ajax.

Supported versions

  • Ruby 1.9.2, 1.9.3

  • Rails 3.0.x, 3.1, 3.2

  • Haml 3+

Install

Put this line in your Gemfile:

gem 'html_slicer'

Then bundle:

% bundle

Implementation

Basic approach

slice <method_name>, <configuration>, [:config => <:style>]*

where:

  • <method_name> - any method name which returns source String (can be called with .send()).

  • <configuration> - Hash of configuration options and/or :config parameter.

Basic example

class Article < ActiveRecord::Base
  slice :content, :as => :paged, :slice => {:maximum => 20}, :resize => {:width => 300}
end

where:

  • :content is an attribute accessor for Article which returns a target String object.

  • :as is a name of basic accessor for result.

  • :slice is a hash of slicing options as a part of configuration.

  • :resize is a hash of resizing options as a part of configuration.

  • :cache_to is an accessor name used to store the cache. True value make it by default.

You can define any configuration key you want. Otherwise, default configuration options (if available) will be picked up automatically.

Console:

@article = Article.find(1)
@article.content
# => "Words like violence break the silence\r\nCome crashing in into my little world\r\n<iframe width=\"560\" height=\"315\" src=\"http://www.youtube.com/embed/ms0bd_hCZsk\" frameborder=\"0\" allowfullscreen></iframe>\r\nPainful to me, pierce right through me\r\nCan't you understand, oh my little girl?"

@article_paged = @article.paged
# => "Words like violence bre"

@article_paged.slice!(2)
# => "ak the silence"

@article_paged.slice!(4)
# => "rld
<iframe width="300" height="169" src="http://www.youtube.com/embed/ms0bd_hCZsk" frameborder="0" allowfullscreen></iframe>"

The .slice!() method accepts number of slice (page). No argument (or nil) assumes it is number 1. Passed slice number is remembered.

Configuration options

All configuration keys:

  • :as is a name of basic accessor for sliced object (result).

  • :slice is a hash of slicing options.

  • :resize is a hash of resizing options.

  • :processors - processors names.

  • :window - parameter for ActionView: The “inner window” size (4 by default).

  • :outer_window - parameter for ActionView: The “outer window” size (0 by default).

  • :left - parameter for ActionView: The “left outer window” size (0 by default).

  • :right - parameter for ActionView: The “right outer window” size (0 by default).

  • :params - parameter for ActionView: url_for parameters for the links (:controller, :action, etc.)

  • :param_name - parameter for ActionView: parameter name for slice number in the links. Accepts Symbol, String, Array.

  • :remote - parameter for ActionView: Ajax? (false by default)

  • :config - special key for using stylized configuration (premature configuration).

Slicing options

  • :unit is a Regexp/String/Hash description of text units counted to split the text by slices.

When value is a Hash, it assumes the unit is a HTML tag (look at :only/:except options for details). Undefined value or nil assumes it by default regular expression /&#?\w+;|\S/. As you can see it counts any regular character or HTML special character as a unit.

  • :maximum is a Fixnum number of units to be a one slice.

If :unit defined as Regexp or String, default value is 300.

If :unit defined as Hash, default value is 10.

If :unit is default, default value is 2000.

  • :complete is a Regexp description of a character used to complete the slice.

For example, if you want to end the slice with a complete word, using :complete => /s+|z/ the counter would continue the slice until the first whitespace character.

  • :limit - a Fixnum limit number of slices.

In many cases we just need the first slice to perform it as a partial.

  • :text_break - a responsible .to_s value of text breaks between slices.

It can be an ellipsis or any other symbol.

  • :only is a Hash or Array of hashes, describes which exactly nodes of HTML content to slice.*

  • :except is a Hash or Array of hashes, describes which exactly nodes of HTML content NOT to slice.*

    This is a very flexible utility to navigate via HTML content. Read native documentation for details.

    For example: ID for <hr class="break"> tag is a hash: {:tag => "hr", :attributes => {:class => "break"}}

Resizing options

  • :width is a Fixnum number of pixels as a target value to squeeze the HTML tag. It does the resize automatically proportional with the ‘height=’ (if the tag has one). The percentage values are ignored.

  • :only is a Hash or Array of hashes, describes which exactly nodes of HTML content to resize.

  • :except is a Hash or Array of hashes, describes which exactly nodes of HTML content NOT to resize.

Processors

Processors are used to transform the source text before it is sliced. Many of us use various markup languages for dynamic contents. This is basically the same thing. Just create any class and inherit it from HtmlSlicer::Processor, put it in /lib/html_slicer_processors directory and define its name within the :processors option.

Example:

# /lib/html_slicer_processors/textilized.rb:

class Textilized < HtmlSlicer::Processor
  def make
    ERB::Util.textilize(content)
  end
end

Processor class has to include make method, which returns transformed String. The content accessor is a source String. Then you can set the option :processors => :textilized. The value of option can be Array of names, if you use more than one processor.

Configuration levels

There are three levels of configuration. Missing options are inherited from upper-level ones.

Global configuration

The top level configuration contains necessary options by default:

window = 4
outer_window = 0
left = 0
right = 0
param_name = :slice

You can override/complete global configuration this way:

HtmlSlicer.configure do |config|
  config.slice = {:complete => /\s+|\z/, :maximum => 2000}
  config.resize = {:width => 300, :only => {:tag => 'iframe'}}
  config.window = 5
  config.param_name = :any_other_param_name
end

Stylized configuration

Along with common global configuration, you can define stylized configuration passing argument like this:

HtmlSlicer.configure(:paged) do |config|
  config.as = :page
  config.param_name = :page
end

Missing options are inherited from global one.

Instance configuration

If you implement HtmlSlicer accessor this way:

slice <method_name>, <configuration>, [:config => <:style>]*

you can override/complete any configuration.

By applying :config key you can use one of stylized configurations:

class Post < ActiveRecord::Base
  slice :content, :config => :paged, :slice => {:complete => /\s+/, :maximum => 3000}
end

Through passing the block you get a more flexible approach to configure slicer:

class Post < ActiveRecord::Base
  slice :content, :config => :paged, :as => :blabla do |config|
    config.slice[:maximum] = 3000 
    config.resize.merge! :only => {:tag => "p"}
    config.processors << :decorated
  end
end

Views

Presumption

Model:

class Article < ActiveRecord::Base
  slice :content, :as => :paged, :slice => {:maximum => 1000, :complete => /\s+|\z/}
end

Controller:

@article = Article.find(1)
@article_paged = @article.paged.slice!(params[:slice])

On a View just call the slice helper:

<%= slice @article_paged %>

This will render several ?slice=N slicing links surrounded by an HTML5 <nav> tag.

Helpers

  • the slice helper method

    <%= slice @article_paged %>
    

    This would output several slicing links such as « First ‹ Prev ... 2 3 4 5 6 7 8 9 10 ... Next › Last »

  • specifying the “inner window” size (4 by default)

    <%= slicing @article_paged, :window => 2 %>
    

    This would output something like ... 5 6 7 8 9 ... when 7 is the current page.

  • specifying the “outer window” size (0 by default)

    <%= slicing @article_paged, :outer_window => 3 %>
    

    This would output something like 1 2 3 4 ...(snip)... 17 18 19 20 while having 20 slices in total.

  • outer window can be separately specified by left, right (0 by default)

    <%= slicing @article_paged, :left => 1, :right => 3 %>
    

    This would output something like 1 ...(snip)... 18 19 20 while having 20 slices in total.

  • changing the parameter name (:param_name) for the links

    <%= slicing @article_paged, :param_name => :page %>
    

    This would modify the query parameter name on each links.

  • extra parameters (:params) for the link

    <%= slicing @article_paged, :params => {:controller => 'foo', :action => 'show', :id => 21} %>
    

    This would modify each link’s url_option. :controller and :action might be the common keys.

  • Ajax links (crazy simple, but works perfectly!)

    <%= slicing @article_paged, :remote => true %>
    

    This would add data-remote="true" to all the links inside.

  • the link_to_next_page helper method

    <%= link_to_next_slice @article_paged, 'Next Page' %>
    

    This simply renders a link to the next slice. This would be helpful for creating “Twitter like” pagination feature.

  • the slice_entries_info helper method

    <%= slice_entries_info @article_paged %>
    

    This renders a helpful message with numbers of displayed vs. total entries.

I18n and labels

The default labels for ‘first’, ‘last’, ‘previous’, ‘…’ and ‘next’ are stored in the I18n yaml inside the engine, and rendered through I18n API. You can switch the label value per I18n.locale for your internationalized application. Keys and the default values are listed below. You can override them by adding to a YAML file in your Rails.root/config/locales directory.

en:
  views:
    html_slicer:
      first: "&laquo; First"
      last: "Last &raquo;"
      previous: "&lsaquo; Prev"
      next: "Next &rsaquo;"
      truncate: "..."

Param name

You can define :param_name as a symbol or string, or as an array of any object that responses .to_s method and returns string. Passing array is the way to define nested :param_name.

Examples:

:param_name => :page
# means you define params[:page] as a slice key.

:param_name => [:article, :page]
# means you define params[:article][:page] as a slice key.

Caching

Caching implies that resizing/slicing procedures is both time-consuming processes will be started only at once, and once again will be launched only if the target content has been changed or resizing/slicing options has been modified.

For caching, pass the option: :cache_to => true or :cache_to => :any_other_accessor within your config definition.

ActiveRecord model

Example:

class Article < ActiveRecord::Base
  slice :content, :as => :paged, :slice => {:maximum => 1000, :complete => /\s+|\z/}, :cache_to => :content_page_cache
end

Accessor method .content_page_cache= used for caching here. The first time when resizing and slicing procedures is called, generated maps will be cached and assigned to .content_page_cache accessor. Before the article saves itself, assigned dump stuff is recorded like any other attribute of article (callback before_save is set up).

Of course, attribute is recorded only as a column of the database :), So, before, add the column to a model:

% rails generate migration AddContentPageCacheToArticle content_page_cache:text

% rake db:migrate

Slicing/resizing procedures repeat again only if target content has been changed or options has been modified.

ActiveModel

Example:

class TextModel
  attr_accessor :text, :paged_cache

  extend HtmlSlicer::Installer

  slice :text, :as => :paged, :slice => {:complete => /\s+/, :maximum => 300}, :cache_to => true

  def initialize(text)
    @text = text
  end

end

True value of :cache_to option set default cache accessor name consisted of basic accessor name + _page.

In fact, caching ActiveModel not so significant in most cases, but still works. The next time you call slicing method, the cached resizing/slicing map(s) will be used.

More

Passing the option :text_break => "..." is for mainly decorative purpose. Within the slice (page), the final textual content is complemented by ellipsis or any other symbol you like. If we’d like to place the URL link at the end, we can pass static link as well (“Read mode” or something…), but what if we’d like to pass dynamic link generated within the view using the full power of ActionView and @template?

So, we have:

@article = Article.find(1)
@article_paged = @article.paged.slice!(params[:slice])

On a View we call .to_s() method and pass it the block:

<%= @article_paged.to_s {|slicer, text| text << link_to("Read more", url_for(url), :class => "read-more")} %>

where:

  • slicer is a slicer object itself.

  • text is a final textual content of the current slice (page) which we complete with the generated link inside the block code.

Slicing a general String or ActiveModel (or any other) object

There is no apecific approach to it. Just extend target class to HtmlSlicer::Installer and call the method slice as described before:

String.extend HtmlSlicer::Installer
String.slice :to_s, :as => :page, :config => :for_string

Questions, Feedback

Message me and I’ll do my best to help everybody. Github (addagger), no Twitter account, Facebook (www.facebook.com/valery.kvon)

Contributing to HtmlSlicer

  • Fork, fix, and then send me a pull request.

Copyright © 2012 Valery Kvon. See MIT-LICENSE for further details.