LE Blog

Инженер с поэтической душой

30.04.2009 firtree_right Тестирование Paperclip с помощью Factory Girl

Введение

Регулярно в своей работе я встречаюсь с новыми инструментами и решаю разные задачи. Иногда новые решения кажутся незначительными и слишком простыми для того, чтобы посвящать им целый пост в блоге. Но в нашем блоггерском деле главное — это регулярность. :)

Задача

Протестировать вновь изученный инструмент Paperclip с помощью вновь изученного инструмента Factory Girl.

Решение

Допустим, прикрепленный файл называется photo. То есть в модели написано следующее:

class Something < ActiveRecord::Base
  has_attached_file :photo
end

Кладем файл с фото, например, в папку с fixtures. Тогда файл test/factories.rb (или spec/factories.rb) будет содержать:

Factory.define :something do |smth|
  smth.photo File.new("path_to_file_in_fixtures", "rb")
end

И всё! Теперь в тестах, когда нужно создать объект пишем:

Factory.create(:something)
 # или
Factory.build(:something)

А в тестах контроллера пишем:

Factory.attributes_for(:something)

Выводы

Если вы хотите написать проект за короткое ограниченное время, то лучше всего использовать инструменты, которые вы уже опробовали и знаете.

Если вы хотите изучить новые инструменты, то лучше всего сделать небольшой проектик с их использованием.

Проекты в первом и втором случае значительно отличаются по объему. :)

Материалы для изучения

Множество прекрасных инструментов от ThoughtBot

20.04.2009 firtree_right Тестирование OpenID с помощью Cucumber

Задача

Какое-то время назад решил приобщиться к bdd (behavior driven development). С помощью такого подхода очень удобно описывать функционал приложения с точки зрения финального пользователя. И, соответственно, писать код так, чтобы выполнялись сценарии.

Для этого подхода существует прекрасный инструмент Cucumber, сценарии которого пишутся непосредственно текстом. Их, соответственно, можно даже обсуждать в качестве технического задания.

В качестве хорошего примера рассмотрим сценарий комментирования в этом блоге. Комментарии здесь можно оставлять только с помощью идентификации по OpenID.

Ресурсы

Нам понадобится, собственно, Cucumber, который для более лёгкого старта лучше использовать вместе с RSpec (фреймворк для удобного написания тестов и test driven development) и WebRat (инструмент для интегральных тестов, который содержит удобные методы работы с клиентским интерфейсом приложения).

Для работы с OpenID со стороны конечного пользователя нам понадобится локальный тестовый сервер OpenID. Есть хороший вариант: ROTS.

Затребуем библиотеки в тестовом окружении. В файле config/environments/test.rb:

config.gem "rspec", :lib => false, :version => ">=1.2.4"
config.gem "rspec-rails", :lib => false, :version => ">=1.2.4"
config.gem "webrat", :lib => false, :version => ">=0.4.4"
config.gem "cucumber", :lib => false, :version => ">=0.3.0"
config.gem "roman-rots", :lib => "rots", :version => ">=0.2.1"

Установим всё это:

sudo apt-get install libxml2-dev libxslt-dev
gem sources -a http://gems.github.com
rake gems:install RAILS_ENV=test

Решение

Предполагается, что как написать аутентификацию через OpenID мы знаем. Наша задача сейчас только протестировать функционал. В любом случае, когда мы приступаем к данному сценарию, у нас уже должны быть описаны и запрограммированы статьи и хотя бы создана модель для комментариев.

Теперь чтобы начать использовать Cucumber в приложении нужно запустить генератор:

script/generate cucumber

Процесс разработки в bdd выглядит так: сначала пишем сценарий, потом пишем всё остальное. Но в данном посте я просто покажу результат.

OpenID сервер

После установки библиотеки ROTS появляется консольная комманда:

rots

При запуске тестов необходимо, чтобы сервер был запущен. Для работы нам хватит параметров по умолчанию. В данном сервере уже есть пользователь, параметры для которого мы и будем использовать в тестах.

Сценарий Сucumber

Сценарии называются features. Создаем файл features/create_comments.feature:

Feature: Create Comments
  In order to give feedback
  As a reader
  I want to create comments

  Scenario: Create Comment
    Given I visit a page for the published post with 1 approved comment
    When I fill in "OpenID" with "http://localhost:1123/john.doe?openid.success=true"
    And I fill in "Имя" with "John Doe"
    And I fill in "Текст" with "Nice post!"
    And I press "Отправить"
    Then I should verify my OpenID
    And I should see "John Doe"
    And I should see "Nice post!"
    And the post should have 2 approved comments

Многие из шагов уже определены с помощью WebRat и RSpec. Доопределим то, чего не хватает.

Шаги для статей

Тут есть моё внутреннее соглашение (с самим собой :). Когда я использую определенный артикль the, то имею в виду статью, которую я запоминаю в течение каждого сценария. В файле features/step_definitions/post_steps.rb:

Given /^I visit a page for the (published|draft) post with (\d+) (approved )?comments?$/ do |pub, cmnts_count, appr|
  @post = Post.create!(:title => "Cucumber test post",
                       :body => "This is test post for cucumber comments",
                       :published => (pub == "published"))
  cmnts_count.to_i.times do |cnt|
    comment = @post.comments.build(:name => "Test User", :body => "Test body #{cnt}")
    comment.openid_url = "http://test.example"
    comment.approved = true unless appr.nil?
    comment.save!
  end
  visit post_url(@post)
end

Then /^the post should have (\d+) (approved )?comments?$/ do |count, appr|
  if appr.nil?
    @post.comments.count.should == count.to_i
  else
    @post.approved_comments.count.should == count.to_i
  end
end

Шаги для комментариев

Этот шаг специально написан для работы с ROTS, который, не требуя от пользователя никакого интерактивного взаимодействия в процессе теста отвечает на запрос редиректом подтверждая или отвергая авторизацию. В файле features/step_definitions/comment_steps.rb:

Then /^I should verify my OpenID$/ do
  response = Net::HTTP.get_response(URI.parse(headers['location']))
  response.class.should == Net::HTTPSeeOther
  visit response['location']
end

Заключение

Вот и всё. Чтобы запустить тест, нужно выполнить:

cucumber features

Далее, конечно же, следует написать сценарии для пустых полей, для отказа в авторизации, для неправильного адреса OpenID и прочее. Но это, как мне кажется, не составляет труда.

Так же известно, что Cucumber поддерживает и русский язык. То есть сценарии можно писать и на русском. Но я пока не пробовал.

Материалы для изучения

RailsCast про авторизацию с помощью OpenID RailsCast про использование Cucumber Документация по Cucumber

14.04.2009 firtree_right Фильтрация rss-потоков с помощью Sinatra и HTTParty

Задача

Для фильтрации 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

28.03.2009 firtree_right Автосохранение с помощью JavaScript

О стандартизации

Ещё недавно я говорил о том, что не очень люблю JavaScript за то, что результат работает неодинаково в разных браузерах разных версий и ратовал за Flash и иже с ним, как за прекрасную альтернативу. Но судьба предоставила мне шанс убедиться, что и Flash с его ActionScript вполне может себя вести непредсказуемо в различный браузерах и разных версиях плеера, если программа достаточно сложная.

Я сразу же вспомнил о своём научном руководителе, который в школе курировал мою работу для поступления в ВУЗ. В своей работе он использовал FORTRAN. И на наши недоумённые вопросы отвечал, что это чуть ли не единственный язык, для которого можно записать текст программы любой сложности на дискетту и перенести на любой компьютер любой платформы. Видимо, так оно до сих пор и есть.

Задача

Чтобы отдать дань не очень справедливо заклейменному JavaScript, я решил написать для своего блога и для блога Иры автосохранение.

Ресурсы

Для работы нам понадобится Prototype, который входит в стандартную поставку Rails и обязательно крутилочка. Без крутилочки не стоит даже начинать. :)

Решение

Решение оказалось не очень сложное, поэтому я просто его опубликую с дополнительными пояснениями по мере надобности.

config/routes.rb

map.resources :posts, :member => {:autosave => :post}

Контроллер

...
def autosave
  @post = Post.find(params[:id])
  if @post.update_attributes(params[:post])
    render :nothing => true
  else
    render :nothing => true, :status => 400
  end
end
...

Шаблон

Привожу только код JavaScript, который понадобится для сохранения. Рекомендую обратить внимание, на решение проблемы authenticity_token

var cachedBody = $("post_body").value;
var cachedTitle = $("post_title").value;
var saveTimeout = 20000;
var modTimeout = 200;
var requestInProgress = false;

function checkSave() {
  if ((((cachedBody != $("post_body").value) && ($("post_body").value != "")) ||
       ((cachedTitle != $("post_title").value) && ($("post_title").value != ""))) &&
      !requestInProgress) {
    cachedTitle = $("post_title").value;
    cachedBody = $("post_body").value;
    $("spinner").show();
    requestInProgress = true;
    new Ajax.Request("<%= autosave_post_url(@post) -%>", {
        method: "post",
        postBody: Form.serializeElements([$("post_title"), $("post_body")]) +
              '&authenticity_token=' + encodeURIComponent('<%= form_authenticity_token -%>'),
        onSuccess: function (transport) {
              var now = new Date();
              $("spinner").hide();
              $("autosave_text").innerHTML = "Saved at " + 
                    (now.getHours() < 10 ? "0" : "") + now.getHours() + ":" +
                    (now.getMinutes() < 10 ? "0" : "") + now.getMinutes() + ":" +
                    (now.getSeconds() < 10 ? "0" : "") + now.getSeconds();
              setTimeout("checkSave()", saveTimeout);
              requestInProgress = false;
            },
        onFailure: function (transport) {
              $("spinner").hide();
              $("autosave_text").innerHTML = "Failed to autosave";
              requestInProgress = false;
            }
      });
  } else {
    setTimeout("checkSave()", saveTimeout);
  }
}

function checkMod() {
  if (((cachedBody != $("post_body").value) && ($("post_body").value != "")) ||
      ((cachedTitle != $("post_title").value) && ($("post_title").value != ""))) {
    if ($("autosave_text").innerHTML.charAt(0) != "*") {
      $("autosave_text").innerHTML = "* " + $("autosave_text").innerHTML;
    }
  }
  setTimeout("checkMod()", modTimeout);
}

function checks() {
  checkSave();
  checkMod();
}

document.observe('dom:loaded', checks);

В данном случае spinner — это id крутилочки. Крутилочка показывается, когда проходит запрос. Так же здесь имеется дополнительный цикл вызовов. Раз уж мы всё равно храним копию того, что сохранено на сервере, то почему бы не показывать, когда текст уже изменен, но не сохранен на сервере. С помощью звездочки.

А как сделать, чтобы звездочка исчезала, если текст в полях снова стал такой, оставлю на самостоятельное изучение.

Материалы для изучения

Интересный пример ненавязчивого JavaScript для создания «бесконечной» страницы

24.03.2009 firtree_right Jabber бот

Возникла идея написать jabber-бота. Выбор протокола обусловлен тем, что по нему достаточно много документации. И стандарт открытый — это хорошо. Много информации на code.google.com, но я решил пойти более простым путём и нашёл библиотеку на руби xmpp4r. Благодаря тому, что разработка ведется на руби, время разработки простого бота значительно сокращается. Устанавливается как библиотека:

sudo gem i xmpp4r

Регистрация

Это прекрасно, что бот может сам себя зарегистрировать.

require "rubygems"
require "xmpp4r"

include Jabber

client = Client.new(JID.new("login@server"))
client.connect

begin
  client.register "secretpassword"
  puts "Success!"
rescue ServerError => e
  puts "Error: #{e.error.text}"
end

При регистрации можно передавать хэш с параметрами для визитной карточки. После регистрации для последующей авторизации вместо register вызываеься:

client.auth "secretpassword"

Список контактов

Список контактов называется roster. Чтобы с ним работать нужно его запросить:

require "xmpp4r/roster"
roster = Roster::Helper.new(client)

А после этого обработать ответы при обратных вызовах. Если добавлять друзей будет тоже бот, и не будет заморачиваться с группами контактов, то для вывода списка друзей на данный момент достаточно описать вызов:

roster.add_query_callback do |iq|
  puts "current roster:"
  roster.find_by_group(nil).each do |item|
    puts "#{item.jid}"
  end
end

Простейшее разрешение видеть свой статус и добавление в контакты в ответ на то, что добавляют бота выглядит так:

roster.add_subscription_request_callback do |item, pres|
  puts "Added by #{pres.from}"
  roster.accept_subscription(pres.from) # Разрешить видеть свой статус
  new_pres = Presence.new.set_type(:subscribe).set_to(pres.from)
  client.send(new_pres) # Запросить разрешение видеть статус
end

Теперь наши контакты смогут видеть наш бот онлайн, когда мы сообщаем об этом:

client.send(Presence.new.set_type(:available))

Неплохо бы что-нибудь ещё добавить, типа аватара.

Визитная карточка

Визитная карточка называется vcard:

require "xmpp4r/vcard"
require "base64"

vcard = Vcard::IqVcard.new
vcard["FN"] = "Full Name"
vcard["NICKNAME"] = "bot-nickname"
vcard["PHOTO/TYPE"]= "image/jpeg"
avatar_file = File.new("avatar.jpg", "r")
avatar_b64 = Base64.b64encode(avatar_file.read())
avatar_file.rewind
avatar_sha1 = Digest::SHA1.hexdigest(avatar_file.read())
vcard["PHOTO/BINVAL"] = avatar_b64
avatar_file.close

begin
  Vcard::Helper.new(client).set(vcard)
  puts "Success!"
rescue Exception => e
  puts "VCARD operation failed: #{e.to_s}" 
end

В соответствии со спецификацией xmpp относительно аватаров следует транслировать с какой-то периодичностью sha своего аватара:

Thread.new do
  while true do
    unless avatar_sha1.nil?
      pres = Presence.new
      x = REXML::Element.new("x")
      x.add_namespace("vcard-temp:x:update")
      photo = REXML::Element.new("photo")
      avatar_hash = REXML::Text.new(avatar_sha1)
      photo.add(avatar_hash)
      x.add(photo)
      pres.add_element(x)
      client.send(pres)
    end
    sleep 60
  end
end

Дело за малым.

Ответ на сообщения

cl.add_message_callback do |m|
  if m.type != :error and m.body
    client.send(m.answer.set_body(bot_main_function(m.body)))
  end
end

Внутри bot_main_function находится собственно то, что будет делать ваш бот, как он будет отвечать на то, что написали ему.

Материалы для изучения

Ещё интересные примеры

16.03.2009 firtree_right Автоматизация процесса публикации

Когда я впервые прочитал про, например, Capistrano, мне, конечно же, сразу захотелось тоже начать применять эту клёвую штуку. Но я, конечно же, не преодолел барьер входа. На тот момент у меня было полтора приложения на ruby on rails, которые я довольно редко обновлял. Позже, когда я начал регулярно обновлять несколько приложений, использовать средства автоматизации оказалось очень просто и очень естественно. Для этого достаточно было вручную обновить приложение раз двадцать. :)

Задача

Допустим, речь идёт не о веб-приложении, а о библиотеке, которая используется на сервере несколькими веб-приложениями и другими программами, которые так же исполняются на сервере. Вполне логичным представляется сделать её в виде rubygem. И тогда встает вопрос обновления этой библиотеки на сервере.

Если делать это вручную достаточно долго, то со временем, после упрощений и оптимизаций, становится понятно, что для обновления нужно зайти на сервер по ssh и выполнить простую комманду:

cd somedir && do_some_stuff && sudo do_some_sudo_stuff

Использовать для этого любую готовую библиотеку публикации веб-приложений кажется слишком громостким. Так почему бы не написать задачу для rake, которая бы делала именно то, что нужно.

Ресурсы

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

sudo gem i net-ssh highline

Решение

Первым делом я, конечно, попробовал:

Net::SSH.start("myserver", "sudouser") do |ssh|
  result = ssh.exec!("cd somedir && do_some_stuff && sudo do_some_sudo_stuff")
  puts result
end

Но никакого вывода просто не дождался. Потому что дойдя до sudo-команды, процесс просто оставался в вечном ожидании.

Чтобы сделать ввод пароля, нужно создавать канал. А так же неплохо было бы проверить возможность интерактивного взаимодействия:

Net::SSH.start("myserver", "sudouser") do |ssh|
  channel = ssh.open_channel do |ch|
    ch.request_pty do |c, success|
      raise "Cannot obtain pty" unless success
    end
    ...
  end
end

Теперь нужно отправить пароль в нужный момент. Чтобы узнать, когда наступил нужный момент, нужно использовать ключ -p (prompt) при вызове sudo, чтобы сказать ему, каким запросом спрашивать у нас пароль.

sudo -p 'sudo password: ' do_some_sudo_stuff

Когда нужно будет запросить пароль, воспользуемся библиотекой highline:

pwd = HighLine.new.ask("Input remote host sudo password: ") { |q| q.echo = false }

Это позволит нам получить пароль, не светя его на экране. Как это обычно и делает sudo.

Теперь посмотрим на всё решение целиком. В папке библиотеки создаем файл Rakefile. Записываем в него нашу задачу. В моём случае команда для сервера состояла примерно из следующего набора: «Перейти в папку, обновить исходники из scm, собрать джем, sudo установить джем, sudo удалить установленные старые версии джема».

Rakefile

require 'rubygems'
require 'rake'
require "net/ssh"
require 'highline'
...
desc "Update gem on the server by current version on remote origin"
task :deploy do
  Net::SSH.start("myserver", "sudouser") do |ssh|
    channel = ssh.open_channel do |ch|
      ch.request_pty do |c, success|
           # Если pseudo-tty недоступен, то невозможно никакого интерактива
        raise "Cannot obtain pty" unless success
      end

      ch.exec("cd somedir && do_some_stuff && sudo -p 'sudo password: ' do_some_sudo_stuff") do |c, success|
        abort "Could not execute command" unless success

        c.on_data do |c, data|
          if data =~ /sudo password: /
            pwd = HighLine.new.ask("Input remote host sudo password: ") { |q| q.echo = false }
            c.send_data "#{pwd}\n"
          else
            c[:result] ||= ""
            c[:result] << data # Можно, конечно, и в процессе выводить
          end
        end

        c.on_extended_data do |c, type, data|
          puts "STDERR : #{data}"
        end
      end
    end
    ssh.loop # Ожидаем, пока закончится сеанс
    puts channel[:result] # Выводим результат сеанса (можно было и в процессе)
  end
end
...

Теперь вместо всей той последовательности действий достаточно написать запустить rake deploy и ввести пароль.

Материалы для изучения

Пособия по использованию Capistrano Документация Net::SSH

11.03.2009 firtree_right Использование GEdit для разработки на Ruby и Flex

Сегодня я хочу поделиться своими ресурсами для разработки. В частности, хочу рассказать про IDE. Основную часть рабочего времени я провожу под кубунту. После долгих метаний и проб я остановился на GEdit, как на универсальном средстве разработки.

Установка

Для работы нам понадобятся плагины для GEdit. Они содержат основной набор вкусностей: code snippets, file browser pane, terminal pane и тому подобное. Поэтому устанавливаем:

sudo apt-get install gedit gedit-plugins

Дополнительные файлы, которые понадобятся, находятся в теле поста.

Ruby и Ruby on Rails

Подсветка синтаксиса и сниппеты для ruby уже поставляются из коробки. Единственное, если вы откроете файл со спецификацией подсветки синтаксиса для ruby, то обнаружите там пометку FIXME, касающуюся подсветки интерполяции внутри строки. Это связано с особенностями обработки правил подсветки самим редактором. Эти особенности удалось обойти, присвоив этому правилу измененный цвет фона.

Подсветку синтаксиса Ruby положить в /usr/share/gtksourceview-2.0/language-specs/ Для корректной работы подсветки, потребуется определить стиль для ruby:interpolation. Я использую тему darkmate, в которой определил необходимые дополнительные цвета на свой вкус. Положить следует в /usr/share/gtksourceview-2.0/styles/, а затем выбрать эту тему в установках редактора.

Язык для описания подсветки синтаксиса достаточно прост. Кроме всего прочего он позволяет ссылаться из правил одного языка на правила другого. Что, собственно, нам и понадобится для подсветки языка темплейтов erb. Ведь erb — это по сути html со вставками ruby. Теперь, когда у нас есть оба описания, берём описание для html и вставляем в него ссылку на ruby.

Так же следует определить mime-type для erb. Правила подсветки erb предполагают, что в системе определен mime-type text/erb. В четвертом KDE описать свои типы файлов можно в System->System Settings->Advanced->File Associacions

Мой файл использует цвет erb:background, который так же определен в файле с темой.

Подсмотрев code snippets для других языков, а так же в процессе использования, вполне можно самостоятельно написать пару-тройку полезных выражений.

Flash и Flex

Для работы с flex нам понадобится flex sdk, а так же отладочная версия standalone flash player. Я уж не буду вдаваться в подробности, как там написать компилирующий или запускающий скрипт. А выложу сразу подсветку синтаксиса.

Если определить тип (mime-type) text/x-shockwave, то можно использовать правила для подсветки ActionScript3. Которые следует положить по адресу, указаному выше.

Так же, используя описанное выше знание, легко создать описание подсветки синтаксиса mxml, зная, что это xml, со вставками actionscript. Для корректной работы следует определить mime-type text/mxml.

Заключение

Все файлы, выложенные в посте, можно смело изменять под свои нужды. Если вы используете GEdit, чтобы писать под рельсы или флекс. Я не стал выкладывать сниппеты, потому что на мой взгляд, это вопрос личной привычки. А подсветку синтаксиса, конечно, можно улучшать.

02.03.2009 firtree_right Пространства имён XML в ActionScript 3

В последнее время достаточно плотно сталкивался с обработкой XML в ActionScript 3. В третьей версии ActionScript для обработки XML реализована спецификация ECMAScript for XML (E4X). Это значительно упрощает работу. Обращение к дочерним элементам происходит так же, как обращение к свойствам объекта. Об этом я и хочу рассказать в этот раз, взяв для примера что-то более сложное, чем в стандартный примерах.

var xml:XML =
    <robot xmlns="http://robots.org/2069/body/"
              xmlns:color="http://robots.org/2075/bodycolor"
              xmlns:extra="http://extraparts.org/2080/xml"
              xmlns:uni="http://universal.org/2010/robots/xml">
    <body>
      <head>
        <brain uni:id="abc123456" />
        <brain uni:id="abc987654" />
        <casing color:rgb="cfa499" />
      </head>
      <extra:additional name="mono">
        <extra:arm>
          <extra:claw extra:size="12" color:pantone="134f" />
        </extra:arm>
      </extra:additional>
    </body>
    </robot>;

То есть в данном примере присутствует четыре спецификации, которые описывают различные (или возможные) элементы или атрибуты XML. Что же будет, если мы попробуем что-нибудь поискать:

trace(xml.body.length()); // 0

Это произошло потому, что сам XML имеет своё пространство имён (namespace). Поэтому можно сделать так:

trace(xml.body.length()); // 0
var mainNS:Namespace = new Namespace("http://robots.org/2069/body/");
trace(xml.mainNS::body.length()); // 1
default xml namespace = mainNS;
trace(xml.body.length()); // 1

Остальные пространства имён:

var colorNS:Namespace = new Namespace("color", "http://robots.org/2075/bodycolor");
var extraNS:Namespace = new Namespace("extra", "http://extraparts.org/2080/xml");
var uniNS:Namespace = new Namespace("uni", "http://universal.org/2010/robots/xml");

Объект, возвращаемый при выборке имеет тип XMLList. Но не смотря на это, дальнейшую выборку можно продолжать. Так же для примера рассмотрим обращение к атрибутам:

trace(xml.body.head.casing.@colorNS::rgb); // cfa499

В случае, когда элементы не в единственном числе и для большей строгости то же самое можно записать следующим образом.

trace(xml.body[0].head[0].casing[0].@colorNS::rgb[0]); // cfa499

Кстати, обращение к атрибуту так же возвращает XMLList. Поиск по атрибуту, если нам не особо важна структура, будет выглядеть следующим образом:

trace(xml..mainNS::brain.(@uniNS::id.indexOf("abc") >= 0).@uniNS::id.toXMLString());
            //abc123456
            //abc987654

То есть после первой выборки по частичному вхождению id вышло два элемента. Когда мы запросили атрибут, оказалось два атрибута. То есть строгое выражение, если нам нужет второй элемент, будет такое:

trace(xml..mainNS::brain.(@uniNS::id.indexOf("abc") >= 0)[1].@uniNS::id);
            //abc987654

Естественно, что XMLList может быть использован для образования цикла с помощью for .. in.

Ещё один способ обращаться к элементам и атрибутам — это с помощью методов класса XML и объекта QName. Это прекрасный способ, если не нужно «хардкодить» имена элементов. Ещё это может пригодиться, если элементы xml имеют имена, совпадающие со служебными или ключевыми словами ActionScript.

var claw:QName = new QName(extraNS, "claw");
var pantone:QName = new QName(colorNS, "pantone");
trace(xml.descendants(claw)[0].attribute(pantone)); // 134f

Материалы для изучения

Документация класса XML со ссылками Руководство пользователя на тему XML в ActionScript 3 Для хардкорных читателей

24.02.2009 firtree_right Регулярные выражения: радость победы 2 :)

Ещё один бонус, который я ожидал от этого блога, и который уже успел получить — это обратная связь. После разговора с Лёшей Кукушкиным было решено модифицировать задачу из предыдущего поста так, чтобы стало удобнее.

Задача

  1. Ввести дополнительный тэг для кода
  2. Печатать код внутри тэга так, как он должен выглядеть (прошлый раз пришлось шаманить)
  3. Иметь возможность более ли менее безнаказанно употреблять сами тэги в тексте

Решение

Итак, для кода будем использовать тэг [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.02.2009 firtree_right Регулярные выражения: радость победы

С регулярными выражениями я знаком не очень хорошо. Поэтому каждый раз, когда нужно что-то сделать, приходится собираться с силами. Но зато когда это сделать удается, наступает радость и счастье.

Задача

Сделать форматирование текста для блога, чтобы:

  1. Можно было вставлять подзаголовки;
  2. Текст разбивался на параграфы и просто переносы строки;
  3. Со вставками кода ничего не происходило;
  4. Было написано на 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("<", "&lt;").gsub(">", "&gt;") + "</code></pre>"
  end
  res.gsub!("<p></p>", "")
  res += "</p>"
end
...

Материалы для изучения

http://www.regular-expressions.info/ http://regexp.ru/