18 мая 2011, 22:54
Темы: ruby, ruby1.9, regexp, syntax, bash, mistakes
Предыстория
Не далее как прошлой осенью я писал о том, как изнутри процесса определить, запущен ли он, используя инструмент grep. В комментариях мне посоветовали использовать pidof, но мне не удалось заставить его работать для руби, т.к. поиск происходит по имени запускаемого файла, а в случае руби-скрипта это всегда ruby. Но мне существенно удалось сократить получение списка запущенных процессов с таким же именем. Вместо:
`ps ax | grep #{File.basename(__FILE__)} | grep -v grep`.split("\n").map{ |l| l.strip.split(/\s+/)[0].to_i }.reject{ |pid| pid == Process.pid }
получилось
`pgrep -f #{File.basename(__FILE__)}`.chomp.split(/\s+/).reject{ |pid| pid.to_i == Process.pid }
Довольно часто мне необходимо отфильтровать вывод или содержимое файла хитрее, чем просто поиск по регулярному выражению. Поскольку мне очень нравится руби, и, как неоднократно писалось в этом блоге, я пытаюсь использовать его везде, где можно, то почему бы снова так не поступить?

Командная строка руби
Руби имеет умеренное количество ключей командной строки. Кратко они описаны в выводе:
ruby --help
Нас в большей степени интересуют ключи -n и -p, которые создают цикл вокруг чтения из пайпа. Ссылка на подробности в конце статьи.
Например, мы хотим посчитать, сколько всего виртуальной памяти занимают все процессы браузера гугл-хром. В качестве источника информации будем использовать вывод команды:
ps axo "%p %z %c"
В которой собраны только необходимые данные (занимаемая виртуальная память и имя процесса без аргументов) и пид (ну а вдруг?). А теперь этот вывод отправим не грепу, а нашему родному руби:
ps axo "%p %z %c" | ruby -nae 'num ||= 0; num += $F[1].to_i if $F[2] =~ /chrome/; END{puts "total chrome virtual memory size #{num} Kbytes"}'
Что это означает? Ключ n означает, что вокруг нашего скрипта есть цикл вида:
while gets(); ... end
Ключ a означает, что вместо переменной $_, куда автоматически попадает результат gets, мы можем использовать $F, который есть суть $_.split. А END содержит блок, который выполняется после цикла.
Ту же магию можно использовать и внутри запускаемых руби-скриптов. Например, если мы хотим найти какое-то слово внутри файла, выделить его цветом и вывести строку с номером, где это слово нашлось, то наш скрипт будет выглядеть вот так (файл look_for):
#!/usr/bin/ruby -n
BEGIN {
unless ARGV.size == 2
puts "Usage: ./look_for <word> <path/to/file>"
exit
end
str = ARGV.shift
}
next unless $_ =~ /#{str}/
printf "%6s%s", $., $_.gsub($&, "\e[31m#{$&}\e[0m")
Теперь, если сделать этот файл запускаемым и запустить его:
./look_for word /in/some/file
То можно увидеть неземную красоту. Кстати, обратите внимание на shift. Без него программа не работает, т.к. gets, который тут за кадром правит бал, пытается воспринимать все аргументы как пути к файлам, из которых непременно нужно что-нибудь прочитать.
Прочие прекрасные применения параметров командной строки руби я предлагаю пытливому читателю подсмотреть в ссылках ниже или найти самостоятельно.
Материалы для самостоятельного изучения
- Полный код статьи на гитхабе.
- Справочник по параметрам командной строки.
- То же, что и выше, но подробнее
- Множество прекрасных примеров (со ссылкой на источник).
23 марта 2011, 18:58
Темы: ruby, ruby1.9, regexp, syntax
Пролог
Что-то большие перерывы в написании статей входят в привычку. Способность некоторых коллег по цеху регулярно выдавать что-нибудь полезное и интересное вызывает уважение.

Введение
С тех самых пор, как я только узнал про регулярные выражения, я слышал об их несовершенстве и моральном устаревании. Регулярные выражения продолжали использоваться, а недовольные теоретики сетовать. Основной претензией было то, что регулярные выражения не позволяют исследовать вложенности паттернов в виду своей линейности. Действительно, соглашался я, невозможно проверить правильность открытия и закрытия тегов или получить выражение в самых внутренних скобках.
Однако, как оказалось, человечество шагнуло далеко вперёд в вопросе совершенствования регулярных выражений. Об одном из новшеств хочу сегодня рассказать.

Именованные группы
В регулярных выражениях руби 1.9 появились именованные группы. Вот, как выглядит их элементарное использование:
if /\A(?<first>[a-zA-Z]+)\s+(?<last>[a-zA-Z]+)\Z/ =~ "Vassily Poopkine"
puts [first, last].inspect
end
if md = /\A(?<first>[a-zA-Z]+)\s+(?<last>[a-zA-Z]+)\Z/.match("Vassily Poopkine")
puts [md[:first], md[:last]].inspect
end
То есть мы не только выделяем группу скобками, как обычно, назначая ей тем самым порядковый номер (по номеру открывающей скобки), но и даём имя. И использовать его можно не только в локальных переменных и объекте MatchData, но и в самом регулярном выражении.
Более того, обращение к объявленным группам внутри может быть рекурсивным. Мне сразу же захотелось написать давнишнюю мою задумку о функции, раскрывающей вложенные скобки. Вот так:
str = "1 + 2 * (3 - 4 / {5 + 6} + [7 - 8 * (9 + 10 * 11) + 12 * {13 - 14}] + 15) + 16 * (17 + 18)"
re = %r{
(?<fill>[0-9+\-*/\s]+){0}
(?<expression>\g<fill>*\g<brackets>\g<fill>*|\g<fill>){0}
(?<braces>\{\g<expression>+\}){0}
(?<squarebrackets>\[\g<expression>+\]){0}
(?<parentheses>\(\g<expression>+\)){0}
(?<brackets>\g<braces>|\g<squarebrackets>|\g<parentheses>)
}x
def calculator(str)
if str =~ /\A[0-9+\-*\/\s]+\Z/
eval str
else
raise "Invalid expression: #{str}"
end
end
f =-> s do
if $~[:expression] == $~[:fill]
calculator($~[:fill])
else
calculator($~[:brackets][1..-2].gsub(re, &f))
end
end
puts calculator(str.gsub(re, &f))
puts eval(str.gsub(/(?<left>\{|\[)|\}|\]/) { |s| $~[:left] ? "(" : ")" })
Итак, в регулярном выражении присутствует 6 именованных групп: fill (заполнения пространства между скобками), expression (выражение, содержащее одни или ни одних нераскрытых скобок), braces (фигурные скобки), squarebrackets (квадратные скобки), parentheses (круглые скобки), brackets (любые скобки). Как видите, выражение описывается через скобки, а скобки через выражение.
Для проверки правильности расчёта, используем обычный eval, заменив все скобки на круглые.

Сделав этот пример, я был доволен, как стадо слонов, но потом решил проверить, а что будет, если скобки расставлены неправильно?
str = "1 + 2 * (3 - 4 / {5 + 6} + [7 - 8 * (9 + 10 * 11) + 12 * {13 - 14]} + 15) + 16 * (17 + 18)"
re = %r{
(?<fill>[0-9+\-*/\s]+){0}
(?<expression>\g<fill>*\g<brackets>\g<fill>*|\g<fill>){0}
(?<braces>\{\g<expression>+\}){0}
(?<squarebrackets>\[\g<expression>+\]){0}
(?<parentheses>\(\g<expression>+\)){0}
(?<brackets>\g<braces>|\g<squarebrackets>|\g<parentheses>)
}x
str =~ re
И я не смог дождаться завершения работы оператора =~ для такого длинного выражения. Это, конечно, неприятно. В причины я вникал не особо, но похоже, это связано с поведением недетерминированной машины Тьюринга. По крайней мере вот ответ на похожую проблему. Для нас это всего лишь означает, что проверять правильность расстановки скобок нужно отдельно и другим способом. Чем я предлагаю заняться пытливому читателю самостоятельно.
Материалы для самостоятельного изучения
- Исходный код статьи.
- Новый синтаксис и прочие вкусняшки в руби 1.9. Для тех, кто заметил =->.
- Глобальные переменные с непонятными именами. Для тех, кто заметил $~.
- Ещё немного базовых приёмов в регулярных выражениях руби.
11 ноября 2009, 20:46
Темы: ruby, ruby1.9, syntax, regexp
Постановка задачи
Собрать в одном месте важные, на мой взгляд, особенности сравнений и неравенств в руби.
Основа неравенств в руби
Основным методом сравнения является <=>. Определив его, мы определяем все остальные операции, включив модуль Comparable:
class MyComp
attr :value
include Comparable
def initialize(val)
@value = val
end
def <=>(other)
@value <=> other.value
end
end
v1 = MyComp.new(1)
v2 = MyComp.new(2)
puts v1 < v2 # > true
puts v1 <= v2 # > true
puts v1 > v2 # > false
puts v1 >= v2 # > false
puts v1 == v2 # > false
Сам метод можно было бы описать как «возвращает -1, 0 или 1 в зависимости от того, меньше равен или больше объект, чей метод вызывается в сравнении с объектом переданным в качестве параметра». Но на самом деле, скорее, наоборот понятия «больше», «меньше» и «равен» определяются исходя из работы <=>.
Далее всё понятно и более ли менее очевидно для чисел, массивов и строк. Но есть и интересная особенность.
Сравнение модулей и классов
Сравнение для модулей и классов определено таким образом, что в результате мы знаем направление наследования или включение одного модуля другим:
module T1
end
module T2
include T1
end
T3 = T1
class C1
end
class C2 < C1
end
C3 = C1
puts "T1 <=> T2: #{(T1 <=> T2).inspect}" # > 1
puts "T1 <=> T3: #{(T1 <=> T3).inspect}" # > 0
puts "C1 <=> C2: #{(C1 <=> C2).inspect}" # > 1
puts "C1 <=> C3: #{(C1 <=> C3).inspect}" # > 0
puts "C1 <=> T1: #{(C1 <=> T1).inspect}" # > nil
puts "T1 <=> C1: #{(T1 <=> C1).inspect}" # > nil
C3.send(:include, T1)
puts "после включения"
puts "C1 <=> T1: #{(C1 <=> T1).inspect}" # > -1
puts "T1 <=> C1: #{(T1 <=> C1).inspect}" # > 1
Наследник или модуль, который включает другой модуль, меньше, чем родитель или включаемый модуль. Это видно даже из синтаксиса наследования.
Равенство
Существует три метода равенства: ==, eql?, equal?. Последний из которых никогда не следует переопределять, т.к. он отвечает за идентичность. Первые же два обычно работают одинаково. Канонический пример различия из документации:
3 == 3.0 # > true
3.eql? 3.0 # > false
Что лишь свидетельствует о том, что == проводит конвертацию чисел перед сравнением. Обычно == соответствует случаю, когда <=> возвращает 0.
Сравнение case...when
Все мы знаем, что в case...when оператор сравнения это ===. В большинстве случаев он эквивалентен равенству из предыдущего параграфа. Но если равенство симметрично
(a.==(b)) == (b.==(a))
И если это не так, то это можно считать ошибкой. То === вовсе не обязано таковым быть. Нужно помнить, что в конструкции case...when вызывается метод сравнения объекта, стоящего после when, а в качестве параметра ему передаётся объект, стоящий после case:
puts String === "строка" # > true
puts "строка" === String # > false
puts /ок/ === "строка" # > true
puts "строка" === /ок/ # > false
puts (1..10) === 5 # > true
puts 5 === (1..10) # > false
Материалы для самостоятельного изучения
- Полный код статьи на github
- Что нужно помнить, создавая свой объект руби
16 мая 2009, 00:10
Темы: actionscript, regexp, flex, flash
В последнее время моя работа в текущем проекте заключается в отладке, оптимизации и отлове багов. Поэтому в самой работе практически не встречается того, о чём можно было бы здесь написать. Но на помощь пришла гугл-группа ruFlash и комьюнити молодых программистов. :)
Задача
Один из участников попросил составить выражение для удаления из текста ссылок с определенным текстом внутри целиком. Например, в выражении:
var str:String = '<a href="somelink"><i>some text</i></a> ';
str += 'More text! ';
str += '<a href="anotherlink"><b>remove me</b></a> ';
str += '<a href="yetanotherlink"><s>another text</s></a>';
Нужно удалить целиком ссылку, содержащую фразу remove me.
Понятно, что первое приходящее в голову выражение /<a.+?remove me.*?<\/a>/ захватит две первые ссылки. И «жадность» не поможет, т.к. поиск осуществляется по порядку, и, найдя первый <a, выражение не остановится до самого remove me.
Решение номер один
Поскольку к концу недели отладки и пересмотра одного и того же кода голова моя не была готова что-то изобретать, я последовал пути, предлагавшемуся одним из ответивших, слегка его доделав:
var re0:RegExp = /<a[^>]+>[^a]*remove me.*?<\/a>/g;
trace(str.replace(re0, "!removed!"));
Недостаток его очевиден. Хотя для приведенного примера он работает, но всё-таки, может и отказать, если встретит a между > и remove me. Например:
str += '<a href="anotherlink">eh! ah? <b>remove me</b></a> ';
Решение номер два (рекурсивное)
Поскольку к этому моменту мозг ещё не расслабился окончательно и не готов был отказаться от выбранного способа думать о задаче, второе решение, пришедшее сразу за первым, было значительно сложнее.
Оно использовало возможность подсовывать функцию в качестве аргумента. Вот оно:
var re1:RegExp = /<a(.+?remove me.*?<\/a>)/g;
var replacer1:Function = function():String {
var s:String = arguments[1].toString();
if (s.indexOf("<a") > 0) {
return "<a" + s.replace(re1, replacer1);
} else {
return "!removed!";
}
}
trace(str.replace(re1, replacer1));
Здесь речь идёт о том, чтобы в группе (см. скобки), следующей после <a проверять наличие ещё одного <a. И в случае его наличия запускать ту же процедуру замены, но уже на группе.
Довольный собой, я запостил своё решение в ruFlash и поехал домой. По дороге домой мозг окончательно расслабился, и я смог увидеть задачу в отрыве от способа думать, который я выбрал изначально. И мне пришло
Решение номер три
Зачем городить рекурсию, когда можно просто перебирать все ссылки и заменять (удалять) только те, что нужно?
var re2:RegExp = /<a[^>]+>(.+?)<\/a>/g;
var replacer2:Function = function():String {
var s:String = arguments[1].toString();
if (s.indexOf("remove me") > 0) {
return "!removed!";
} else {
return arguments[0];
}
}
trace(str.replace(re2, replacer2));
Это ли не чудесно?
Выводы
- Решайте задачи.
- Решив (или не решив), записывайте то, что получилось, покажите кому-нибудь. Это позволит выкинуть решение из головы.
- Если есть решение лучше, то оно придет на освободившееся место.
07 мая 2009, 23:45
Темы: actionscript, regexp, flex
Введение
Я снова вернулся к работе над флэшовым проектом. Поэтому немного об ActionScript. Описанная здесь задача сейчас мне не кажется такой сложной, какой она казалась, когда я впервые с ней столкнулся. Но тем не менее.
Задача
Имеется строка, содержащая разметку для замены её составляющих. Одна из разметок: #[some_url] должна быть заменена содержимым этого самого some_url. Для замены с помощью регулярных выражений в ActionScript 3 существует функция String#replace. Но всё, что связано с загрузкой из внешних источников, создает асинхронность. А любая попытка остановить код, сделать паузу, приводит к огромной потере производительности и ошибкам, которые генерирует плеер, когда долго не может завершить вызов. «Как быть?» спросит меня пытливый читатель.
Решение
Решение состоит в том, чтобы заменять все вхождения ключевого выражения по-очереди и когда всё заменено, создавать событие.
Приведу основную часть. Файл AsyncStringReplaceExample.as
package {
import flash.events.Event;
import flash.net.URLLoader;
import flash.net.URLRequest;
public class AsyncStringReplaceExample {
public static const RE_URL:RegExp = /#\[([^\]]+)\]/g;
private var _str:String;
private var _currentExpr:String;
private var _ldr:URLLoader;
private var _loaded:Boolean;
public function AsyncStringReplaceExample(str:String) {
_str = str;
_ldr = new URLLoader();
_ldr.addEventListener(Event.COMPLETE, ldrCompleteHandler);
_loaded = false;
}
public function replace():Boolean {
_loaded = true;
_str = _str.replace(RE_URL, replaceURL);
return _loaded;
}
public function get string():String {
return _str;
}
private function replaceURL():String {
if (!_currentExpr) {
_loaded = false;
_currentExpr = arguments[0];
_ldr.load(new URLRequest(arguments[1]));
}
return arguments[0];
}
private function ldrCompleteHandler(evt:Event):void {
_str = _str.replace(_currentExpr, evt.target.data);
_currentExpr = null;
if (replace()) {
trace(_str); // здесь желанное событие
}
}
}
}
Теперь остается только использовать написанное нами богатство:
var str:String = "строка для примера\n";
str += "добавим: #[http://some.url/file.txt] или";
str += "ещё добавим: #[http://another.url/another/file.txt]! хватит?";
var asyncString:AsyncStringReplaceExample = new AsyncStringReplaceExample(str);
if (asyncString.replace()) {
trace(asyncString.string); //не нужно ничего заменять
} else {
asyncString.addEventListener(......)
}
...
// внутри обрабочика событий
trace(asyncString.string);
Выводы
Сразу видно, что последнее время я забросил ActionScript и занимался больше Ruby. Потому что подсветка синтаксиса в Ruby красивее. Но это не страшно. :)
Упражнения
Мне было бы интересно написать функцию, которая могла бы работать сколь угодно долго. Например, рекурсивную, или долгую по любой другой причине. Но ActionScript принудительно завершает процессы, которые долго не подают признаков жизни.
Как быть?
14 апреля 2009, 16:30
Темы: ruby, rack, regexp, xml
Задача
Для фильтрации rss-потоков сужествует множество инструментов. Для своей задачи мне захотелось написать простейшее решение и заодно попробовать пару новых инструментов.
Надо: собрать воедино несколько единообразных rss-потоков, отфильтровав только нужное, и выдать единый rss-поток.
Для удобства предположим, что потоки имеют одинаковый формат atom. Адреса нужных нам потоков будут находиться в текстовом файле, разделенные переносом строки. Так же как и необходимые нам ключевые слова. Так же допустим, что наличие ключевых слов будем отслеживать в заголовках.
Ресурсы
Поскольку я собираюсь фильтровать на лету, мне не нужно ничего нигде хранить, я решил попробовать лёгкий руби-фреймворк под названием Sinatra.
А для работы с самими потоками, для получения их с их серверов используем простой и удобный инструмент HTTParty.
sudo gem i sinatra
sudo gem i httparty
Сбор и фильтрация
Создадим библиотечный файл feed_fetcher.rb:
require 'rubygems'
require 'httparty'
class FeedFetcher
include HTTParty
format :xml # позволяет получть результат сразу расфасованный
# в Hash
def self.get_items
urls = nil # будет массив адресов
titles = nil # будет массив нужных частей заголовков
items = [] # будет массив записей
File.open("path_to_feed_urls_file") do |f|
urls = f.readlines.each(&:strip!)
end
File.open("path_to_titles_file") do |f|
titles = f.readlines.each(&:strip!)
end
# составим единое регулярное выражение для фильтрации
retitles = Regexp.union(titles.reject(&:empty?).map { |t| %r{\b#{Regexp.escape(t)}\b}i })
# соберём записи со всех адресов в единый масив
urls.each do |u|
items += get(u)["rss"]["channel"]["item"] unless u.empty?
end
# отфильтруем по регулярному выражению и упорядочим по дате
items.select { |i| i["title"] =~ retitles }.sort do |x, y|
DateTime.parse(y["pubDate"]) <=> DateTime.parse(x["pubDate"])
end
end
end
Выдача результата
Результат будем так же выдавать в формате atom, поэтому нам понадобится builder, который, например, входит в состав active_support. Но можно установить его и отдельно.
Файл feed_filter.rb:
require 'rubygems'
require 'sinatra'
require 'active_support'
require 'feed_fetcher.rb'
get '/' do
content_type 'application/xml', :charset => 'utf-8'
@items = FeedFetcher.get_items
builder :index
end
По-умолчанию Sinatra хранит шаблоны в папке views. Файл views/index.builder:
xml.instruct!
xml.rss "version" => "2.0", "xmlns:atom" => "http://www.w3.org/2005/Atom" do
xml.channel do
xml.title "My Filtered Feed"
xml.link "http://lonelyelk.com"
xml.pubDate CGI::rfc1123_date Time.parse(@items.first["pubDate"]) if @items.any?
xml.description "Some description"
@items.each do |item|
xml.item do
item.each_pair do |key, value|
xml.tag!(key, value)
end
end
end
end
end
Запуск приложения с помощью passenger
Для запуска приложения мы будем использовать Passenger, который поддерживает не только rails, но и rack. Для этого нам понадобится создать папку public и указать к ней путь.
В установках виртуального сервера для apache:
<VirtualHost *:80>
ServerAdmin webmaster@mydomain.ru
ServerName feedfilter.mydomain.ru
DocumentRoot /path/to/feed_filter/public
...
</VirtualHost>
А в папке приложения нужно создать файл config.ru:
require 'rubygems'
require 'sinatra'
Sinatra::Application.set(:run, false)
Sinatra::Application.set(:environment, ENV['RACK_ENV'])
require 'feed_filter'
run Sinatra::Application
Вот и всё. Естественно, ещё следует написать тесты. Так же для публикации можно использовать capistrano. Но это, я думаю, всем под силу.
Материалы для изучения
Первое знакомство с фреймворком Sinatra
24 февраля 2009, 23:10
Темы: ruby, regexp, rails
Ещё один бонус, который я ожидал от этого блога, и который уже успел получить это обратная связь. После разговора с Лёшей Кукушкиным было решено модифицировать задачу из предыдущего поста так, чтобы стало удобнее.
Задача
- Ввести дополнительный тэг для кода
- Печатать код внутри тэга так, как он должен выглядеть (прошлый раз пришлось шаманить)
- Иметь возможность более ли менее безнаказанно употреблять сами тэги в тексте
Решение
Итак, для кода будем использовать тэг [code]...[/code]. Для того, чтобы не провоцировать преобразование кода там, где не нужно, будет использовать знак «/». (То есть в этом абзаце жирным на самом деле написано «/[code]...[/code]»)
Так же используем стандартный метод rails для приведения в порядок того, что находится внутри тэга [code]. Результат выглядит вот так:
def lonelyelk_format(text)
res = "<p>" + text.to_s.dup
codes = []
res.gsub!(/[^\/]\[code\]([\s\S]+?)\[\/code\]/) do |s|
codes.push(s.gsub(/(^[^\/]\[code\]|\[\/code\]$)/, ""))
"#{s[0,1]}[code#{codes.length - 1} /]"
end
res.gsub!(/\r\n?/, "\n")
res.gsub!(/\n*\[h\]\n*/, "</p><h2>")
res.gsub!(/\n*\[\/h\]\n*/, "</h2><p>")
res.gsub!(/\n\n+/, "</p><p>")
res.gsub!(/\n(?=\[code\d+\s\/\])/, "</p><p>")
res.gsub!(/(\[code\d+\s\/\])\n/, '\1</p><p>')
res.gsub!("\n", "<br />")
res += "</p>"
res.gsub!(/<p>\[code\d+\s\/\]<\/p>/) do |s|
"<pre><code>#{h codes[s.gsub(/\D+/, '').to_i]}</code></pre>"
end
res.gsub!("<p></p>", "")
res.gsub!("/[", "[")
res
end
Итоги
По результатам могу сказать, что код ещё можно улучшать и дорабатывать для разных целей. Чем, безусловно, мне ещё предстоит заниматься. Но писать об этой задаче больше не буду. Есть много другого интересного, о чём можно поговорить.
23 февраля 2009, 17:31
Темы: ruby, regexp
С регулярными выражениями я знаком не очень хорошо. Поэтому каждый раз, когда нужно что-то сделать, приходится собираться с силами. Но зато когда это сделать удается, наступает радость и счастье.
Задача
Сделать форматирование текста для блога, чтобы:- Можно было вставлять подзаголовки;
- Текст разбивался на параграфы и просто переносы строки;
- Со вставками кода ничего не происходило;
- Было написано на ruby.
Использовать RedCloth не хотелось, а стандартное форматирование не подходило. Поэтому приступим.
Вытащить код
Для того, чтобы не делать лишних проверок, вытаскиваем код из страницы. Код находится внутри тэга <pre>. Первое, что приходит на ум, это выражение типа «<pre> слева, </pre> справа и ни одного </pre> посередине.». Но оказалось, что исключить выражение невозможно (по крайней мере, я не нашёл способа). Выражение типа
/<pre>[^(<\/pre>)]+<\/pre>/
По крайней мере в ruby, интерпретируется как «тэг <pre>, внутри которого не встревается ни "<", ни "p", ни "r"... и т.д.»
Для этого понадобится концепция «жадности». То есть:
/<pre>.+<\/pre>/
Cоответствует куску от первого «<pre>» до последнего «</pre>». А нам нужно жадное:
/<pre>.+?<\/pre>/
То есть до ближайшего.
Теперь про wild card. Оказалось, что точка не включает перенос строки. Поэтому нам понадобится что-то более дикое. Wild, wild card. На эту роль подходит [\s\S]/: пробельный символ или непробельный.
Итак, вытаскивание кусков кода выглядит так:
codes = []
res.gsub!(/<pre>[\s\S]+?<\/pre>/) do |s|
codes.push(s)
"code#{codes.length - 1}"
end
Вокруг кусков кода
Дальше задачи попроще. Приведение переноса строки к единому виду, замена выбранных выражений для заголовков на тэги заголовков, замена двух и более переносов строки на параграф. Это не представляет особых сложностей. Меня интересует, чтобы параграф кончился до кода и начался после, даже если там всего один перенос строки.
Что касается «кончился до», то тут используется lookahead (то есть операция при условии, что впереди есть что-то):
res.gsub!(/\n(?=code\d+)/, "</p><p>")
А чтобы начать параграф после куска кода, нам понадобится lookbehind (то есть операция при условии, что перед совпадением есть что-то), который в ruby не работает (по крайней мере в версии 1.8.7). поэтому здесь мы используем группы. И включим группу в результат:
res.gsub!(/(code\d+)\n/, '\1</p><p>')
Видите, вот этот \1?
Остались мелочи: вставить обратно куски кода. Убрать пустые параграфы и параграфы, окружающие куски кода. И вы видите то, что обрабатывает текст этого сообщения.
application_helper.rb:
...
def lonelyelk_format(text)
res = "<p>" + text.to_s.dup
codes = []
res.gsub!(/<pre>[\s\S]+?<\/pre>/) do |s| # вытаскиваем куски кода
codes.push(s)
"code#{codes.length - 1}"
end
res.gsub!(/\r\n?/, "\n") # приводим перево каретки к одному виду
res.gsub!(/\n*\[h\]\n*/, "</p><h2>") # заголовки начало [h]
res.gsub!(/\n*\[\/h\]\n*/, "</h2><p>") # заголовки конец [/h]
res.gsub!(/\n\n+/, "</p><p>") # более одного переноса строки - параграф
res.gsub!(/\n(?=code\d+)/, "</p><p>") # параграф перед кодом
res.gsub!(/(code\d+)\n/, '\1</p><p>') # параграф после кода
res.gsub!("\n", "<br />") # единичный перенос строки
res.gsub!(/(<p>)?code\d+(<\/p>)?/) do |s| # вставляем код обратно
codes[s[4,1].to_i] # здесь ошибка :)
end
res.gsub!("<p></p>", "") # убираем пустые параграфы
res += "</p>"
end
...
Остается одна проблема. Нельзя написать в тексте поста выражение «сode{цифры}». Но для этого просто можно генерировать случайный маркер, которого точно нет в тексте вместо «code».
Обновление
После того, как я попытался написать данный пост, я обнаружил ещё ряд интересных особенностей поведения кода и браузера. А так же нашёл ошибку. Публикую финальный код без пояснений:
application_helper.rb:
...
def lonelyelk_format(text)
res = "<p>" + text.to_s.dup
codes = []
res.gsub!(/<pre><code>[\s\S]+?<\/code><\/pre>/) do |s|
codes.push(s.gsub(/(^<pre><code>|<\/code><\/pre>$)/, ""))
"code#{codes.length - 1}"
end
res.gsub!(/\r\n?/, "\n")
res.gsub!(/\n*\[h\]\n*/, "</p><h2>")
res.gsub!(/\n*\[\/h\]\n*/, "</h2><p>")
res.gsub!(/\n\n+/, "</p><p>")
res.gsub!(/\n(?=code\d+)/, "</p><p>")
res.gsub!(/(code\d+)\n/, '\1</p><p>')
res.gsub!("\n", "<br />")
res.gsub!(/(<p>)?code\d+(<\/p>)?/) do |s|
"<pre><code>" + codes[s.gsub(/\D/, "").to_i].to_s.gsub("<", "<").gsub(">", ">") + "</code></pre>"
end
res.gsub!("<p></p>", "")
res += "</p>"
end
...
Материалы для изучения
http://www.regular-expressions.info/
http://regexp.ru/