Создание постоянных ссылок из строк в Ruby

Posted by Dmytro Shteflyuk on under Ruby & Rails · English (18,877 views)

Если вы разрабатываете приложение на Ruby on Rails вроде блога, возможно вам понадобится генерировать URLы используя заголовки статей. Это хорошая практика, потому что поисковики обожают ключевые слова в URLах, да и выглядят они более человеко-читабельными. Просто сравните: http://example.com/posts/10 и http://example.com/posts/generating-permalinks-from-string (угу, длинновато, но зато наглядно). А любом случае, это короткая заметка о преобразовании заголовка в постоянную ссылку.

Основная вещь, которую я обожаю в Ruby — это возможность расширять классы своими собственными методами. Я могу просто добавить метод to_permalink любой строке, и затем где угодно использовать что-то вроде @post.title.to_permalink. Это офигенно!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class String
  def to_permalink
    result = strip_tags
    # Preserve escaped octets.
    result.gsub!(/-+/, '-')
    result.gsub!(/%([a-f0-9]{2})/i, '--\1--')
    # Remove percent signs that are not part of an octet.
    result.gsub!('%', '-')
    # Restore octets.
    result.gsub!(/--([a-f0-9]{2})--/i, '%\1')

    result.gsub!(/&.+?;/, '-') # kill entities
    result.gsub!(/[^%a-z0-9_-]+/i, '-')
    result.gsub!(/-+/, '-')
    result.gsub!(/(^-+|-+$)/, '')
    return result.downcase
  end

  private
 
    def strip_tags
      return clone if blank?
      if index('<')
        text = ''
        tokenizer = HTML::Tokenizer.new(self)

        while token = tokenizer.next
          node = HTML::Node.parse(nil, 0, 0, token, false)
          # result is only the content of any Text nodes
          text << node.to_s if node.class == HTML::Text
        end
        # strip any comments, and if they have a newline at the end (ie. line with
        # only a comment) strip that too
        text.gsub(/<!--(.*?)-->[\n]?/m, '')
      else
        clone # already plain text
      end
    end
end

Как это работает? Первое, что вы скорее всего заметили,– это приватный метод strip_tags. Да, я знаю об ActionView::Helpers::TextHelper::strip_tags, и это почти 100% копия версии из Rails (отличие только в том, что мой код всегда возвращает копию исходной строки). Я просто не хочу зависеть от рельсовых библиотек.

Далее мой метод заменяет все специальные символы на дефисы (остаются только октеты вроде %A0), и обрезает дефисы в начале и в конце строки. Ну и перед выходом строка будет приведена к нижнему регистру.

Естественно, в вашем приложении вы должны проверить коллизии (несколько статей с одинаковыми заголовками должны иметь уникальные постоянные ссылки, например вы можете добавлять числа начиная с 1: hello, hello-1, hello-2 и т.д.). У меня нет желания описывать все сложности, с которыми вы можете столкнуться, это ведь короткая заметка, вы же помните?

Удовольствия ради, вот тесты RSpec для этого метода:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
describe 'String.to_permalink from extensions.rb' do
  it 'should replace all punctuation marks and spaces with dashes' do
    "!.@#$\%^&*()Test case\n\t".to_permalink.should == 'test-case'
  end

  it 'should preserve _ symbol' do
    "Test_case".to_permalink.should == 'test_case'
  end
 
  it 'should preserve escaped octets and remove redundant %' do
    'Test%%20case'.to_permalink.should == 'test-%20case'
  end

  it 'should strip HTML tags' do
    '<a href="http://example.com">Test</a> <b>case</b>'.to_permalink.should == 'test-case'
  end

  it 'should strip HTML entities and insert dashes' do
    'Test&nbsp;case'.to_permalink.should == 'test-case'
  end

  it 'should trim beginning and ending dashes' do
    '-. Test case .-'.to_permalink.should == 'test-case'
  end

  it 'should not use ---aa--- as octet' do
    'b---aa---b'.to_permalink.should == 'b-aa-b'
  end
 
  it 'should replace % with -' do
    'Hello%world'.to_permalink.should == 'hello-world'
  end

  it 'should not modify original string' do
    s = 'Hello,&nbsp;<b>world</b>%20'
    s.to_permalink.should == 'hello-world%20'
    s.should == 'Hello,&nbsp;<b>world</b>%20'

    s = 'Hello'
    s.to_permalink.should == 'hello'
    s.should == 'Hello'
  end
end

Прикольно, правда?

5 Responses to this entry

Subscribe to comments with RSS

demjan @
said on Май 18, 2007 at 20:59 · Permalink

Да. ))
Интересно сколько займет ресурсов и как будет выглядеть реализация на php в свете недавнего обсуждения на харахабарачтототам

said on Май 18, 2007 at 21:30 · Permalink

Да ровно столько же! Все эти войны Something vs. Something another — чистой воды треп людей, которым нечем заняться. При грамотном подходе код в любом языке будет лаконичным и эффективным. Вопрос в компактности и читабельности — это да, целиком ложится на совесть языка.

Вон в комментах к статье о подзапросах я приводил пример кода пхп. Да, задача не повседневная, но классическая, когда надо обработать выборку. И ну никак всего этого уродства не обойти. Можно размазать по коду, это да. Или вынести в отдельный файл и никому не показывать. Но оно останется. Потому что ПХП — это набор знаков препинания, разделенных словами.

demjan @
said on Май 19, 2007 at 08:57 · Permalink

Аналога этому в не обьектном языке не напишешь:

1
2
3
4
5
class String
  def to_permalink
  end
end
'string'.to_permalink

Только вроде такого

1
2
3
function to_permalink ($name) {
}
to_permalink($post->title);
said on Май 19, 2007 at 12:41 · Permalink

Ну почему же. В C# 3.0 можно будет писать extension методы:

1
2
3
4
5
6
7
8
public static class Extensions
{
    public static String ToPermalink(this String s)
    {
    }
}

'Something'.ToPermalink();

Это не аналогия открытости классов руби, а просто синтаксический сахар. Но результаты в общем-то неразличимы невооруженным взглядом :-)

kelyar
said on Июнь 4, 2007 at 11:29 · Permalink

а почему просто не переопределить to_param модели?

Comments are closed

Comments for this entry are closed for a while. If you have anything to say – use a contact form. Thank you for your patience.