julik-rutils 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. data/History.txt +162 -0
  2. data/Manifest.txt +42 -0
  3. data/README.txt +234 -0
  4. data/Rakefile.rb +41 -0
  5. data/TODO.txt +6 -0
  6. data/WHAT_HAS_CHANGED.txt +44 -0
  7. data/bin/gilensize +22 -0
  8. data/bin/rutilize +38 -0
  9. data/init.rb +27 -0
  10. data/lib/countries/countries.rb +1773 -0
  11. data/lib/datetime/datetime.rb +83 -0
  12. data/lib/gilenson/bluecloth_extra.rb +7 -0
  13. data/lib/gilenson/gilenson.rb +665 -0
  14. data/lib/gilenson/helper.rb +34 -0
  15. data/lib/gilenson/maruku_extra.rb +19 -0
  16. data/lib/gilenson/rdiscount_extra.rb +7 -0
  17. data/lib/gilenson/redcloth_extra.rb +42 -0
  18. data/lib/integration/integration.rb +1 -0
  19. data/lib/integration/rails_date_helper_override.rb +127 -0
  20. data/lib/integration/rails_pre_filter.rb +4 -0
  21. data/lib/pluralizer/pluralizer.rb +261 -0
  22. data/lib/rutils.rb +54 -0
  23. data/lib/transliteration/bidi.rb +21 -0
  24. data/lib/transliteration/simple.rb +75 -0
  25. data/lib/transliteration/transliteration.rb +53 -0
  26. data/lib/version.rb +5 -0
  27. data/test/extras/integration_bluecloth.rb +13 -0
  28. data/test/extras/integration_maruku.rb +15 -0
  29. data/test/extras/integration_rails_filter.rb +29 -0
  30. data/test/extras/integration_rails_gilenson_helpers.rb +80 -0
  31. data/test/extras/integration_rails_helpers.rb +85 -0
  32. data/test/extras/integration_rdiscount.rb +15 -0
  33. data/test/extras/integration_redcloth3.rb +18 -0
  34. data/test/extras/integration_redcloth4.rb +19 -0
  35. data/test/run_tests.rb +4 -0
  36. data/test/test_datetime.rb +145 -0
  37. data/test/test_gilenson.rb +396 -0
  38. data/test/test_integration.rb +22 -0
  39. data/test/test_integration_flag.rb +18 -0
  40. data/test/test_pluralize.rb +84 -0
  41. data/test/test_rutils_base.rb +11 -0
  42. data/test/test_transliteration.rb +53 -0
  43. metadata +121 -0
@@ -0,0 +1,83 @@
1
+ # -*- encoding: utf-8 -*-
2
+ module RuTils
3
+ module DateTime
4
+
5
+ def self.distance_of_time_in_words(from_time, to_time = 0, include_seconds = false, absolute = false) #nodoc
6
+ from_time = from_time.to_time if from_time.respond_to?(:to_time)
7
+ to_time = to_time.to_time if to_time.respond_to?(:to_time)
8
+ distance_in_minutes = (((to_time - from_time).abs)/60).round
9
+ distance_in_seconds = ((to_time - from_time).abs).round
10
+
11
+ case distance_in_minutes
12
+ when 0..1
13
+ return (distance_in_minutes==0) ? 'меньше минуты' : '1 минуту' unless include_seconds
14
+
15
+ case distance_in_seconds
16
+ when 0..5 then 'менее 5 секунд'
17
+ when 6..10 then 'менее 10 секунд'
18
+ when 11..20 then 'менее 20 секунд'
19
+ when 21..40 then 'пол-минуты'
20
+ when 41..59 then 'меньше минуты'
21
+ else '1 минуту'
22
+ end
23
+
24
+ when 2..45 then distance_in_minutes.to_s +
25
+ " " + distance_in_minutes.items("минута", "минуты", "минут")
26
+ when 46..90 then 'около часа'
27
+ # исключение, сдвигаем на один влево чтобы соответствовать падежу
28
+ when 90..1440 then "около " + (distance_in_minutes.to_f / 60.0).round.to_s +
29
+ " " + (distance_in_minutes.to_f / 60.0).round.items("часа", 'часов', 'часов')
30
+ when 1441..2880 then '1 день'
31
+ else (distance_in_minutes / 1440).round.to_s +
32
+ " " + (distance_in_minutes / 1440).round.items("день", "дня", "дней")
33
+ end
34
+ end
35
+
36
+ def self.ru_strftime(time, format_str='%d.%m.%Y')
37
+ clean_fmt = format_str.to_s.gsub(/%{2}/, RuTils::SUBSTITUTION_MARKER).
38
+ gsub(/%a/, Date::RU_ABBR_DAYNAMES[time.wday]).
39
+ gsub(/%A/, Date::RU_DAYNAMES[time.wday]).
40
+ gsub(/%b/, Date::RU_ABBR_MONTHNAMES[time.mon]).
41
+ gsub(/%d(\s)*%B/, '%02d' % time.day + '\1' + Date::RU_INFLECTED_MONTHNAMES[time.mon]).
42
+ gsub(/%e(\s)*%B/, '%d' % time.day + '\1' + Date::RU_INFLECTED_MONTHNAMES[time.mon]).
43
+ gsub(/%B/, Date::RU_MONTHNAMES[time.mon]).
44
+ gsub(/#{RuTils::SUBSTITUTION_MARKER}/, '%%')
45
+
46
+ # Теперь когда все нужные нам маркеры заменены можно отдать это стандартному strftime
47
+ time.respond_to?(:strftime_norutils) ? time.strftime_norutils(clean_fmt) : time.to_time.strftime_norutils(clean_fmt)
48
+ end
49
+ end
50
+ end
51
+
52
+ class Date
53
+ RU_MONTHNAMES = [nil] + %w{ январь февраль март апрель май июнь июль август сентябрь октябрь ноябрь декабрь }
54
+ RU_DAYNAMES = %w(воскресенье понедельник вторник среда четверг пятница суббота)
55
+ RU_ABBR_MONTHNAMES = [nil] + %w{ янв фев мар апр май июн июл авг сен окт ноя дек }
56
+ RU_ABBR_DAYNAMES = %w(вск пн вт ср чт пт сб)
57
+ RU_INFLECTED_MONTHNAMES = [nil] + %w{ января февраля марта апреля мая июня июля августа сентября октября ноября декабря }
58
+ RU_DAYNAMES_E = [nil] + %w{первое второе третье четвёртое пятое шестое седьмое восьмое девятое десятое одиннадцатое двенадцатое тринадцатое четырнадцатое пятнадцатое шестнадцатое семнадцатое восемнадцатое девятнадцатое двадцатое двадцать тридцатое тридцатьпервое}
59
+ end
60
+
61
+ class Time
62
+ alias_method :strftime_norutils, :strftime
63
+
64
+ def strftime(fmt)
65
+ RuTils::overrides_enabled? ? RuTils::DateTime::ru_strftime(self, fmt) : strftime_norutils(fmt)
66
+ end
67
+ end
68
+
69
+ class Date
70
+ # Inside rails we have date formatting
71
+ if self.instance_methods.include?('strftime')
72
+ alias_method :strftime_norutils, :strftime
73
+ def strftime(fmt='%F')
74
+ RuTils::overrides_enabled? ? RuTils::DateTime::ru_strftime(self, fmt) : strftime_norutils(fmt)
75
+ end
76
+
77
+ else
78
+ def strftime(fmt='%F')
79
+ RuTils::overrides_enabled? ? RuTils::DateTime::ru_strftime(self, fmt) : to_time.strftime(fmt)
80
+ end
81
+ end
82
+
83
+ end
@@ -0,0 +1,7 @@
1
+ # -*- encoding: utf-8 -*-
2
+ # BlueCloth с поддержкой Gilenson
3
+ class RuTils::Gilenson::BlueClothExtra < BlueCloth
4
+ def to_html(*opts)
5
+ RuTils::Gilenson::Formatter.new(super(*opts)).to_html
6
+ end
7
+ end
@@ -0,0 +1,665 @@
1
+ # -*- encoding: utf-8 -*-
2
+ module RuTils
3
+ module Gilenson
4
+ # Позволяет возвращать класс форматтера при вызове
5
+ # RuTils::Gilenson.new
6
+ def self.new(*args) #:nodoc:
7
+ RuTils::Gilenson::Formatter.new(*args)
8
+ end
9
+
10
+ autoload :BlueClothExtra, File.dirname(__FILE__) + '/bluecloth_extra'
11
+ autoload :RedClothExtra, File.dirname(__FILE__) + '/redcloth_extra'
12
+ autoload :RDiscountExtra, File.dirname(__FILE__) + '/rdiscount_extra'
13
+ autoload :MarukuExtra, File.dirname(__FILE__) + '/maruku_extra'
14
+ autoload :Helper, File.dirname(__FILE__) + '/helper'
15
+ end
16
+ end
17
+
18
+ # ==Что такое Gilenson
19
+ # Обработчик типографских символов в HTML согласно общепринятым правилам.
20
+ # Посвящается П.Г.Гиленсону[http://www.rudtp.ru/lib.php?book=172], благодаря которому русские правила тех.
21
+ # редактуры еще как минимум 20 лет таки останутся бессмысленно старомодными.
22
+ #
23
+ # Gilenson расставит в тексте "умные" правильные кавычки (русские - для кириллицы, английские - для латиницы),
24
+ # заменит "хитрые" пунктуационные символы на entities и отформатирует знаки типа (c), (tm), телефоны и адреса.
25
+ #
26
+ # Gilenson базируется на коде Typografica[http://pixel-apes.com/typografica] от PixelApes,
27
+ # который был приведен к положенному в Ruby стандарту. Основные отличия Gilenson от Typografica на PHP:
28
+ # * работа только и полностью в UTF-8 (включая entities, применимые в XML)
29
+ # * поддержка "raw"-вывода (символов вместо entities) - текст выводимый Gilenson можно верстать на бумаге
30
+ #
31
+ #
32
+ # ==Использование
33
+ # Быстрее всего - через метод ++gilensize++ для любой строковой переменной
34
+ # %{ И вот они таки "приехали"}.gilensize => 'И&#160;вот они&#160;таки &#171;приехали&#187;'
35
+ # Все дополнительные настройки в таком случае передаются форматтеру
36
+ # %{ И вот они таки "приехали"}.gilensize(:laquo=>false) => 'И&#160;вот они&#160;таки "приехали"'
37
+ #
38
+ # Если форматтер надо настроить более тонко, можно использовать его и так:
39
+ # typ = RuTils::Gilenson.new('Эти "так называемые" великие деятели')
40
+ # typ.to_html => 'Эти &#171;так называемые&#187; великие деятели'
41
+ #
42
+ # или как фильтр
43
+ # formatter = RuTils::Gilenson.new
44
+ # formatter.configure(:dash=>true)
45
+ # for string in strings
46
+ # puts formatter.process(string)
47
+ # end
48
+ #
49
+ # ==Настройки
50
+ # Настройки регулируются через методы
51
+ # formatter.dashglue = true
52
+ # или ассоциированным хешем
53
+ # formatter.configure!(:dash=>true, :quotes=>false)
54
+ #
55
+ # Хеш также можно передавать как последний аргумент методам process и to_html,
56
+ # в таком случае настройки будут применены только при этом вызове
57
+ #
58
+ # beautified = formatter.process(my_text, :dash=>true)
59
+ #
60
+ # В параметры можно подставить также ключ :all чтобы временно включить или выключить все фильтры
61
+ #
62
+ # beautified = formatter.process(my_text, :all=>true)
63
+ #
64
+ # Помимо этого можно пользоваться каждым фильтром по отдельности используя метод +apply+
65
+ #
66
+ # Можно менять глифы, которые форматтер использует для подстановок. К примеру,
67
+ # formatter.glyph[:nbsp] = '&nbsp;'
68
+ # заставит форматтер расставлять "традиционные" неразрывные пробелы. Именно это - большая глупость,
69
+ # но другие глифы заменить может быть нужно.
70
+ #
71
+ # ==Настройки форматтера
72
+ # "inches" - преобразовывать дюймы в знак дюйма;
73
+ # "laquo" - кавычки-ёлочки
74
+ # "quotes" - кавычки-английские лапки
75
+ # "dash" - проставлять короткое тире (150)
76
+ # "emdash" - длинное тире двумя минусами (151)
77
+ # "initials" - проставлять тонкие шпации в инициалах
78
+ # "copypaste" - замена непечатных и "специальных" юникодных символов на entities
79
+ # "(c)" - обрабатывать знак копирайта
80
+ # "(r)", "(tm)", "(p)", "+-" - спецсимволы, какие - понятно
81
+ # "acronyms" - сворачивание пояснений к аббревиатурам (пояснение - в скобках после аббревиатуры
82
+ # без пробела). В текстовой версии пояснение будет "приклеено" к аббревиатуре
83
+ # полукруглой шпацией
84
+ # "degrees" - знак градуса
85
+ # "dashglue", "wordglue" - приклеивание предлогов и дефисов
86
+ # "spacing" - запятые и пробелы, перестановка
87
+ # "phones" - обработка телефонов
88
+ # "html" - при false - запрет использования тагов html
89
+ # "de_nobr" - при true все <nobr/> заменяются на <span class="nobr"/>
90
+ # "raw_output" - (по умолчанию false) - при true вместо entities выводятся UTF-символы
91
+ # "skip_attr" - (по умолчанию false) - при true не отрабатывать типографику в атрибутах тегов (title, alt)
92
+ # "skip_code" - (по умолчанию true) - при true не отрабатывать типографику внутри <code/>, <tt/>, CDATA
93
+ class RuTils::Gilenson::Formatter
94
+ attr_accessor :glyph
95
+ attr_accessor :settings
96
+
97
+ SETTINGS = {
98
+ "inches" => true, # преобразовывать дюймы в знак дюйма;
99
+ "laquo" => true, # кавычки-ёлочки
100
+ "quotes" => true, # кавычки-английские лапки
101
+ "dash" => true, # короткое тире (150)
102
+ "emdash" => true, # длинное тире двумя минусами (151)
103
+ "initials" => true, # тонкие шпации в инициалах
104
+ "copypaste" => false, # замена непечатных и "специальных" юникодных символов на entities
105
+ "(c)" => true, # обрабатывать знак копирайта
106
+ "(r)" => true,
107
+ "(tm)" => true,
108
+ "(p)" => true,
109
+ "acronyms" => true, # Акронимы с пояснениями - ЖЗЛ(Жизнь Замечатльных Людей)
110
+ "+-" => true, # спецсимволы, какие - понятно
111
+ "degrees" => true, # знак градуса
112
+ "dashglue" => true, "wordglue" => true, # приклеивание предлогов и дефисов
113
+ "spacing" => true, # запятые и пробелы, перестановка
114
+ "phones" => true, # обработка телефонов
115
+ "html" => true, # разрешение использования тагов html
116
+ "de_nobr" => false, # при true все <nobr/> заменяются на <span class="nobr"/>
117
+ "raw_output" => false, # выводить UTF-8 вместо entities
118
+ "skip_attr" => false, # при true не отрабатывать типографику в атрибутах тегов
119
+ "skip_code" => true, # при true не отрабатывать типографику внутри <code/>, <tt/>, CDATA
120
+ "enforce_en_quotes" => false, # только латинские кавычки
121
+ "enforce_ru_quotes" => false, # только русские кавычки (enforce_en_quotes при этом игнорируется)
122
+ } #:nodoc:
123
+
124
+ SETTINGS.freeze
125
+
126
+ # Глифы, использующиеся в подстановках по-умолчанию
127
+ GLYPHS = {
128
+ :quot => "&#34;", # quotation mark
129
+ :amp => "&#38;", # ampersand
130
+ :apos => "&#39;", # apos
131
+ :gt => "&#62;", # greater-than sign
132
+ :lt => "&#60;", # less-than sign
133
+ :nbsp => "&#160;", # non-breaking space
134
+ :sect => "&#167;", # section sign
135
+ :copy => "&#169;", # copyright sign
136
+ :laquo => "&#171;", # left-pointing double angle quotation mark = left pointing guillemet
137
+ :reg => "&#174;", # registered sign = registered trade mark sign
138
+ :deg => "&#176;", # degree sign
139
+ :plusmn => "&#177;", # plus-minus sign = plus-or-minus sign
140
+ :para => "&#182;", # pilcrow sign = paragraph sign
141
+ :middot => "&#183;", # middle dot = Georgian comma = Greek middle dot
142
+ :raquo => "&#187;", # right-pointing double angle quotation mark = right pointing guillemet
143
+ :ndash => "&#8211;", # en dash
144
+ :mdash => "&#8212;", # em dash
145
+ :lsquo => "&#8216;", # left single quotation mark
146
+ :rsquo => "&#8217;", # right single quotation mark
147
+ :ldquo => "&#8220;", # left double quotation mark
148
+ :rdquo => "&#8221;", # right double quotation mark
149
+ :bdquo => "&#8222;", # double low-9 quotation mark
150
+ :bull => "&#8226;", # bullet = black small circle
151
+ :hellip => "&#8230;", # horizontal ellipsis = three dot leader
152
+ :numero => "&#8470;", # numero
153
+ :trade => "&#8482;", # trade mark sign
154
+ :minus => "&#8722;", # minus sign
155
+ :inch => "&#8243;", # inch/second sign (u0x2033) (не путать с кавычками!)
156
+ :thinsp => "&#8201;", # полукруглая шпация (тонкий пробел)
157
+ :nob_open => '<span class="nobr">', # открывающий блок без переноса слов
158
+ :nob_close => '</span>', # закрывающий блок без переноса слов
159
+ }.freeze
160
+
161
+ # Нормальные "типографские" символы в UTF-виде. Браузерами обрабатываются плохонько, поэтому
162
+ # лучше заменять их на entities.
163
+ VERBATIM_GLYPHS = {
164
+ ' ' => :nbsp,# alt+0160 (NBSP here)
165
+ '«' => :laquo,
166
+ '»' => :raquo,
167
+ '§' => :sect,
168
+ '©' => :copy,
169
+ '®' => :reg,
170
+ '°' => :deg,
171
+ '±' => :plusmn,
172
+ '¶' => :para,
173
+ '·' => :middot,
174
+ '–' => :ndash,
175
+ '—' => :mdash,
176
+ '‘' => :lsquo,
177
+ '’' => :rsquo,
178
+ '“' => :ldquo,
179
+ '”' => :rdquo,
180
+ '„' => :bdquo,
181
+ '•' => :bull,
182
+ '…' => :hellip,
183
+ '№' => :numero,
184
+ '™' => :trade,
185
+ '−' => :minus,
186
+ ' ' => :thinsp,
187
+ '″' => :inch,
188
+ }.freeze
189
+
190
+ # Метка на которую подменяются вынутые теги
191
+ REPLACEMENT_MARKER = RuTils::SUBSTITUTION_MARKER #:nodoc:
192
+
193
+ # Кто придумал &#147;? Не учите людей плохому...
194
+ # Привет А.Лебедеву http://www.artlebedev.ru/kovodstvo/62/
195
+ # Используем символы, потом берем по символам из glyphs форматтера.
196
+ # Молодец mash!
197
+ FORBIDDEN_NUMERIC_ENTITIES = {
198
+ '132' => :bdquo,
199
+ '133' => :hellip,
200
+ '146' => :apos,
201
+ '147' => :ldquo,
202
+ '148' => :rdquo,
203
+ '149' => :bull,
204
+ '150' => :ndash,
205
+ '151' => :mdash,
206
+ '153' => :trade,
207
+ }.freeze
208
+
209
+ # All the unicode whitespace
210
+ UNICODE_WHITESPACE = [
211
+ (0x0009..0x000D).to_a, # White_Space # Cc [5] <control-0009>..<control-000D>
212
+ 0x0020, # White_Space # Zs SPACE
213
+ 0x0085, # White_Space # Cc <control-0085>
214
+ 0x00A0, # White_Space # Zs NO-BREAK SPACE
215
+ 0x1680, # White_Space # Zs OGHAM SPACE MARK
216
+ 0x180E, # White_Space # Zs MONGOLIAN VOWEL SEPARATOR
217
+ (0x2000..0x200A).to_a, # White_Space # Zs [11] EN QUAD..HAIR SPACE
218
+ 0x2028, # White_Space # Zl LINE SEPARATOR
219
+ 0x2029, # White_Space # Zp PARAGRAPH SEPARATOR
220
+ 0x202F, # White_Space # Zs NARROW NO-BREAK SPACE
221
+ 0x205F, # White_Space # Zs MEDIUM MATHEMATICAL SPACE
222
+ 0x3000, # White_Space # Zs IDEOGRAPHIC SPACE
223
+ ].flatten.pack("U*").freeze
224
+
225
+ PROTECTED_SETTINGS = [ :raw_output ] #:nodoc:
226
+
227
+ def initialize(*args)
228
+ @_text = args[0].is_a?(String) ? args[0] : ''
229
+ setup_default_settings!
230
+ accept_configuration_arguments!(args.last) if args.last.is_a?(Hash)
231
+ end
232
+
233
+ # Настраивает форматтер ассоциированным хешем
234
+ # formatter.configure!(:dash=>true, :wordglue=>false)
235
+ def configure!(*config)
236
+ accept_configuration_arguments!(config.last) if config.last.is_a?(Hash)
237
+ end
238
+
239
+ alias :configure :configure! #Дружественный API
240
+
241
+ # Неизвестные методы - настройки. С = - установка ключа, без - получение значения
242
+ def method_missing(meth, *args) #:nodoc:
243
+ setting = meth.to_s.gsub(/=$/, '')
244
+ super(meth, *args) unless @settings.has_key?(setting) #this will pop the exception if we have no such setting
245
+
246
+ return (@settings[setting] = args[0])
247
+ end
248
+
249
+ # Обрабатывает text_to_process с сохранением настроек, присвоенных обьекту-форматтеру
250
+ # Дополнительные аргументы передаются как параметры форматтера и не сохраняются после прогона.
251
+ def process(text_to_process, *args)
252
+ @_text = text_to_process
253
+
254
+ args.last.is_a?(Hash) ? with_configuration(args.last) { to_html } : to_html
255
+ end
256
+
257
+ # Обрабатывает текст, присвоенный форматтеру при создании и возвращает результат обработки
258
+ def to_html
259
+ return '' unless @_text
260
+
261
+ # NOTE: strip is Unicode-space aware on 1.9.1, so here we simulate that
262
+ text = @_text.gsub(/[#{UNICODE_WHITESPACE}]\z/, '').gsub(/\A[#{UNICODE_WHITESPACE}]/, '')
263
+
264
+ # -6. Подмухляем таблицу глифов, если нам ее передали
265
+ glyph_table = glyph.dup
266
+
267
+ if @settings["enforce_ru_quotes"]
268
+ glyph_table[:ldquo], glyph_table[:rdquo] = glyph_table[:laquo], glyph_table[:raquo]
269
+ elsif @settings["enforce_en_quotes"]
270
+ glyph_table[:laquo], glyph_table[:raquo] = glyph_table[:ldquo], glyph_table[:rdquo]
271
+ end
272
+
273
+ # -5. Копируем глифы в ивары, к ним доступ быстр и в коде они глаза тоже не мозолят
274
+ glyph_table.each_pair do | ki, wi |
275
+ instance_variable_set("@#{ki}", wi)
276
+ end
277
+
278
+ # -4. запрет тагов html
279
+ process_escape_html(text) unless @settings["html"]
280
+
281
+ # -3. Никогда (вы слышите?!) не пущать лабуду &#not_correct_number;
282
+ FORBIDDEN_NUMERIC_ENTITIES.dup.each_pair do | key, rep |
283
+ text.gsub!(/&##{key};/, self.glyph[rep])
284
+ end
285
+
286
+ # -2. Чистим copy&paste
287
+ process_copy_paste_clearing(text) if @settings['copypaste']
288
+
289
+ # -1. Замена &entity_name; на входе ('&nbsp;' => '&#160;' и т.д.)
290
+ process_html_entities(text)
291
+
292
+ # 0. Вырезаем таги
293
+ tags = lift_ignored_elements(text) if @skip_tags
294
+
295
+ # 1. Запятые и пробелы
296
+ process_spacing(text) if @settings["spacing"]
297
+
298
+ # 1. лапки
299
+ process_quotes(text) if @settings["quotes"]
300
+
301
+ # 2. ёлочки
302
+ process_laquo(text) if @settings["laquo"]
303
+
304
+ # 3. Инчи
305
+ process_inches(text) if @settings["inches"]
306
+
307
+ # 2b. одновременно ёлочки и лапки
308
+ process_compound_quotes(text) if (@settings["quotes"] && @settings["laquo"])
309
+
310
+ # 3. тире
311
+ process_dash(text) if @settings["dash"]
312
+
313
+ # 3a. тире длинное
314
+ process_emdash(text) if @settings["emdash"]
315
+
316
+ # 5. +/-
317
+ process_plusmin(text) if @settings["+-"]
318
+
319
+ # 5a. 12^C
320
+ process_degrees(text) if @settings["degrees"]
321
+
322
+ # 6. телефоны
323
+ process_phones(text) if @settings["phones"]
324
+
325
+ # 7. Короткие слова и &nbsp;
326
+ process_wordglue(text) if @settings["wordglue"]
327
+
328
+ # 8. Склейка ласт. Тьфу! дефисов.
329
+ process_dashglue(text) if @settings["dashglue"]
330
+
331
+ # 8a. Инициалы
332
+ process_initials(text) if @settings['initials']
333
+
334
+ # 8b. Троеточия
335
+ process_ellipsises(text) if @settings["wordglue"]
336
+
337
+ # 9. Акронимы от Текстиля
338
+ process_acronyms(text) if @settings["acronyms"]
339
+
340
+ # БЕСКОНЕЧНОСТЬ. Вставляем таги обратно.
341
+ reinsert_fragments(text, tags) if @skip_tags
342
+
343
+ # фуф, закончили.
344
+ process_span_instead_of_nobr(text) if @settings["de_nobr"]
345
+
346
+ # заменяем entities на истинные символы
347
+ process_raw_output(text) if @settings["raw_output"]
348
+
349
+ text.strip
350
+ end
351
+
352
+
353
+ # Применяет отдельный фильтр к text и возвращает результат. Например:
354
+ # formatter.apply(:wordglue, "Вот так") => "Вот&#160;так"
355
+ # Удобно применять когда вам нужно задействовать отдельный фильтр Гиленсона, но не нужна остальная механика
356
+ # Последний аргумент определяет, нужно ли при применении фильтра сохранить в неприкосновенности таги и другие
357
+ # игнорируемые фрагменты текста (по умолчанию они сохраняются).
358
+ def apply(filter, text, lift_ignored_elements = true)
359
+ copy = text.dup
360
+ unless lift_ignored_elements
361
+ self.send("process_#{filter}".to_sym, copy)
362
+ else
363
+ lifting_fragments(copy) { self.send("process_#{filter}".to_sym, copy) }
364
+ end
365
+ copy
366
+ end
367
+
368
+ private
369
+
370
+ def setup_default_settings!
371
+ @skip_tags = true;
372
+ @ignore = /notypo/ # regex, который игнорируется. Этим надо воспользоваться для обработки pre и code
373
+
374
+ @glueleft = ['рис.', 'табл.', 'см.', 'им.', 'ул.', 'пер.', 'кв.', 'офис', 'оф.', 'г.']
375
+ @glueright = ['руб.', 'коп.', 'у.е.', 'мин.']
376
+
377
+ # Установки можно менять в каждом экземпляре
378
+ @settings = SETTINGS.dup
379
+
380
+ @mark_tag = REPLACEMENT_MARKER
381
+ # Глифы можено подменять в экземпляре форматтера поэтому копируем их из константы
382
+ @glyph = GLYPHS.dup
383
+
384
+ @phonemasks = [[ /([0-9]{4})\-([0-9]{2})\-([0-9]{2}) ([0-9]{2}):([0-9]{2}):([0-9]{2})/,
385
+ /([0-9]{4})\-([0-9]{2})\-([0-9]{2})/,
386
+ /(\([0-9\+\-]+\)) ?([0-9]{3})\-([0-9]{2})\-([0-9]{2})/,
387
+ /(\([0-9\+\-]+\)) ?([0-9]{2})\-([0-9]{2})\-([0-9]{2})/,
388
+ /(\([0-9\+\-]+\)) ?([0-9]{3})\-([0-9]{2})/,
389
+ /(\([0-9\+\-]+\)) ?([0-9]{2})\-([0-9]{3})/,
390
+ /([0-9]{3})\-([0-9]{2})\-([0-9]{2})/,
391
+ /([0-9]{2})\-([0-9]{2})\-([0-9]{2})/,
392
+ /([0-9]{1})\-([0-9]{2})\-([0-9]{2})/,
393
+ /([0-9]{2})\-([0-9]{3})/,
394
+ /([0-9]+)\-([0-9]+)/,
395
+ ],[
396
+ ':nob_open\1:ndash\2:ndash\3:nbsp\4:\5:\6:nob_close',
397
+ ':nob_open\1:ndash\2:ndash\3:nob_close',
398
+ ':nob_open\1:nbsp\2:ndash\3:ndash\4:nob_close',
399
+ ':nob_open\1:nbsp\2:ndash\3:ndash\4:nob_close',
400
+ ':nob_open\1:nbsp\2:ndash\3:nob_close',
401
+ ':nob_open\1:nbsp\2:ndash\3:nob_close',
402
+ ':nob_open\1:ndash\2:ndash\3:nob_close',
403
+ ':nob_open\1:ndash\2:ndash\3:nob_close',
404
+ ':nob_open\1:ndash\2:ndash\3:nob_close',
405
+ ':nob_open\1:ndash\2:nob_close',
406
+ ':nob_open\1:ndash\2:nob_close'
407
+ ]]
408
+ end
409
+
410
+ # Позволяет получить процедуру, при вызове возвращающую значение глифа
411
+ # def lookup(glyph_to_lookup)
412
+ # return Proc.new { g[glyph_to_lookup] }
413
+ # end
414
+
415
+ # Подставляет "символы" (двоеточие + имя глифа) на нужное значение глифа заданное в данном форматтере
416
+ def substitute_glyphs_in_string(str)
417
+ re = str.dup
418
+ @glyph.each_pair do | key, subst |
419
+ re.gsub!(":#{key.to_s}", subst)
420
+ end
421
+ re
422
+ end
423
+
424
+ # Выполняет блок, временно включая настройки переданные в +hash+
425
+ def with_configuration(hash, &block)
426
+ old_settings, old_glyphs = @settings.dup, @glyph.dup
427
+ accept_configuration_arguments!(hash)
428
+ txt = yield
429
+ @settings, @glyph = old_settings, old_glyphs
430
+
431
+ return txt
432
+ end
433
+
434
+ def accept_configuration_arguments!(args_hash)
435
+
436
+ # Специальный случай - :all=>true|false
437
+ if args_hash.has_key?(:all)
438
+ if args_hash[:all]
439
+ @settings.each_pair {|k, v| @settings[k] = true unless PROTECTED_SETTINGS.include?(k.to_sym)}
440
+ else
441
+ @settings.each_pair {|k, v| @settings[k] = false unless PROTECTED_SETTINGS.include?(k.to_sym)}
442
+ end
443
+ else
444
+
445
+ # Кинуть ошибку если настройка нам неизвестна
446
+ unknown_settings = args_hash.keys.collect{|k|k.to_s} - @settings.keys.collect { |k| k.to_s }
447
+ raise RuTils::Gilenson::UnknownSetting, unknown_settings if unknown_settings.any?
448
+
449
+ args_hash.each_pair do | key, value |
450
+ @settings[key.to_s] = (value ? true : false)
451
+ end
452
+ end
453
+ end
454
+
455
+ # Вынимает игнорируемые фрагменты и заменяет их маркером, выполняет переданный блок и вставляет вынутое на место
456
+ def lifting_fragments(text, &block)
457
+ lifted = lift_ignored_elements(text)
458
+ yield
459
+ reinsert_fragments(text, lifted)
460
+ end
461
+
462
+ #Вынимает фрагменты из текста и возвращает массив с фрагментами
463
+ def lift_ignored_elements(text)
464
+ # re = /<\/?[a-z0-9]+("+ # имя тага
465
+ # "\s+("+ # повторяющая конструкция: хотя бы один разделитель и тельце
466
+ # "[a-z]+("+ # атрибут из букв, за которым может стоять знак равенства и потом
467
+ # "=((\'[^\']*\')|(\"[^\"]*\")|([0-9@\-_a-z:\/?&=\.]+))"+ #
468
+ # ")?"+
469
+ # ")?"+
470
+ # ")*\/?>|\xA2\xA2[^\n]*?==/i;
471
+
472
+ re_skipcode = '((<(code|tt)[ >](.*?)<\/(code|tt)>)|(<!\[CDATA\[(.*?)\]\]>))|' if @settings['skip_code']
473
+ re = /(#{re_skipcode}<\/?[a-z0-9]+(\s+([a-z]+(=((\'[^\']*\')|(\"[^\"]*\")|([0-9@\-_a-z:\/?&=\.]+)))?)?)*\/?>)/uim
474
+ tags = text.scan(re).map{ |tag| tag[0] } # первая группа!
475
+ text.gsub!(re, @mark_tag) #маркер тега, мы используем Invalid UTF-sequence для него
476
+ return tags
477
+ end
478
+
479
+ def reinsert_fragments(text, fragments)
480
+ fragments.each do |fragment|
481
+ fragment.gsub!(/ (href|src|data)=((?:(\')([^\']*)(\'))|(?:(\")([^\"]*)(\")))/uim) do
482
+ " #{$1}=" + $2.gsub(/&(?!(#0*38)|(amp);)/, @amp)
483
+ end # unless @settings['raw_output'] -- делать это надо всегда (mash)
484
+
485
+ unless @settings['skip_attr']
486
+ fragment.gsub!(/ (title|alt)=((?:(\')([^\']*)(\'))|(?:(\")([^\"]*)(\")))/uim) do
487
+ " #{$1}=#{$3}" + self.process($4.to_s) + "#{$5}#{$6}" + self.process($7.to_s) + "#{$8}"
488
+ end
489
+ end
490
+ text.sub!(@mark_tag, fragment)
491
+ end
492
+ end
493
+
494
+ ### Имплементации фильтров
495
+ def process_html_entities(text)
496
+ self.glyph.each { |key, value| text.gsub!(/&#{key};/, value)}
497
+ end
498
+
499
+ def process_initials(text)
500
+ initials = /([А-Я])[\.]{1,2}[\s]*?([А-Я])[\.]*[\s]*?([А-Я])([а-я])/u
501
+ replacement = substitute_glyphs_in_string('\1.\2.:thinsp\3\4')
502
+ text.gsub!(initials, replacement)
503
+ end
504
+
505
+ def process_copy_paste_clearing(text)
506
+ VERBATIM_GLYPHS.each {|key,value| text.gsub!(/#{Regexp.escape(key)}/, glyph[value]) }
507
+ end
508
+
509
+ def process_spacing(text)
510
+ text.gsub!( /(\s*)([,]*)/sui, '\2\1');
511
+ text.gsub!( /(\s*)([\.?!]*)(\s*[ЁА-ЯA-Z])/su, '\2\1\3');
512
+ end
513
+
514
+ def process_dashglue(text)
515
+ text.gsub!( /([a-zа-яА-Я0-9]+(\-[a-zа-яА-Я0-9]+)+)/ui, @nob_open+'\1'+ @nob_close)
516
+ end
517
+
518
+ def process_escape_html(text)
519
+ text.gsub!(/&/, @amp)
520
+ text.gsub!(/</, @lt)
521
+ text.gsub!(/>/, @gt)
522
+ end
523
+
524
+ def process_span_instead_of_nobr(text)
525
+ text.gsub!(/<nobr>/, '<span class="nobr">')
526
+ text.gsub!(/<\/nobr>/, '</span>')
527
+ end
528
+
529
+ def process_dash(text)
530
+ text.gsub!( /(\s|;)\-(\s)/ui, '\1'+@ndash+'\2')
531
+ end
532
+
533
+ def process_emdash(text)
534
+ text.gsub!( /(\s|;)\-\-(\s)/ui, '\1'+@mdash+'\2')
535
+ # 4. (с)
536
+ text.gsub!(/\([сСcC]\)((?=\w)|(?=\s[0-9]+))/u, @copy) if @settings["(c)"]
537
+ # 4a. (r)
538
+ text.gsub!( /\(r\)/ui, '<sup>'+@reg+'</sup>') if @settings["(r)"]
539
+
540
+ # 4b. (tm)
541
+ text.gsub!( /\(tm\)|\(тм\)/ui, @trade) if @settings["(tm)"]
542
+ # 4c. (p)
543
+ text.gsub!( /\(p\)/ui, @sect) if @settings["(p)"]
544
+ end
545
+
546
+ def process_ellipsises(text)
547
+ text.gsub!( '...', @hellip)
548
+ end
549
+
550
+ def process_laquo(text)
551
+ text.gsub!( /\"\"/ui, @quot * 2);
552
+ text.gsub!( /(^|\s|#{@mark_tag}|>|\()\"((#{@mark_tag})*[~0-9ёЁA-Za-zА-Яа-я\-:\/\.])/ui, '\1' + @laquo + '\2');
553
+ _text = '""';
554
+ until _text == text do
555
+ _text = text;
556
+ text.gsub!( /(#{@laquo}([^\"]*)[ёЁA-Za-zА-Яа-я0-9\.\-:\/\?\!](#{@mark_tag})*)\"/sui, '\1' + @raquo)
557
+ end
558
+ end
559
+
560
+ def process_quotes(text)
561
+ text.gsub!( /\"\"/ui, @quot*2)
562
+ text.gsub!( /\"\.\"/ui, @quot+"."+@quot)
563
+ _text = '""';
564
+ lat_c = '0-9A-Za-z'
565
+ punct = /\'\!\s\.\?\,\-\&\;\:\\/
566
+
567
+ until _text == text do
568
+ _text = text.dup
569
+ text.gsub!( /(^|\s|#{@mark_tag}|>)\"([#{lat_c}#{punct}\_\#{@mark_tag}]+(\"|#{@rdquo}))/ui, '\1'+ @ldquo +'\2')
570
+ text.gsub!( /(#{@ldquo}([#{lat_c}#{punct}#{@mark_tag}\_]*).*[#{lat_c}][\#{@mark_tag}\?\.\!\,\\]*)\"/ui, '\1'+ @rdquo)
571
+ end
572
+ end
573
+
574
+ def process_compound_quotes(text)
575
+ text.gsub!(/(#{@ldquo}(([A-Za-z0-9'!\.?,\-&;:]|\s|#{@mark_tag})*)#{@laquo}(.*)#{@raquo})#{@raquo}/ui, '\1' + @rdquo);
576
+ end
577
+
578
+ def process_degrees(text)
579
+ text.gsub!( /-([0-9])+\^([FCС])/, @ndash+'\1'+ @deg +'\2') #deg
580
+ text.gsub!( /\+([0-9])+\^([FCС])/, '+\1'+ @deg +'\2')
581
+ text.gsub!( /\^([FCС])/, @deg+'\1')
582
+ end
583
+
584
+ def process_wordglue(text)
585
+ text.replace(" " + text + " ")
586
+ _text = " " + text + " "
587
+
588
+ until _text == text
589
+ _text = text
590
+ text.gsub!( /(\s+)([a-zа-яА-Я0-9]{1,2})(\s+)([^\\s$])/ui, '\1\2' + @nbsp +'\4')
591
+ text.gsub!( /(\s+)([a-zа-яА-Я0-9]{3})(\s+)([^\\s$])/ui, '\1\2' + @nbsp+'\4')
592
+ end
593
+
594
+ # Пунктуация это либо один из наших глифов, либо мемберы класса. В данном случае
595
+ # мы цепляемся за кончик строки поэтому можум прихватить и глиф тоже
596
+ # Пунктуация включает наши собственные глифы!
597
+ punct = glyph.values.map{|v| Regexp.escape(v)}.join('|')
598
+ vpunct = /(#{punct}|[\)\]\!\?,\.;])/
599
+
600
+ text.gsub!(/(\s+)([a-zа-яА-Я0-9]{1,2}#{vpunct}{0,3}\s$)/ui, @nbsp+'\2')
601
+
602
+ @glueleft.each { | i | text.gsub!( /(\s)(#{i})(\s+)/sui, '\1\2' + @nbsp) }
603
+
604
+ @glueright.each { | i | text.gsub!( /(\s)(#{i})(\s+)/sui, @nbsp+'\2\3') }
605
+
606
+ end
607
+
608
+ def process_phones(text)
609
+ @phonemasks[0].each_with_index do |pattern, i|
610
+ replacement = substitute_glyphs_in_string(@phonemasks[1][i])
611
+ text.gsub!(pattern, replacement)
612
+ end
613
+ end
614
+
615
+ def process_acronyms(text)
616
+ acronym = /\b([A-ZА-Я][A-ZА-Я0-9]{2,})\b(?:[(]([^)]*)[)])/u
617
+ if @settings["raw_output"]
618
+ text.gsub!(acronym, '\1%s(\2)' % @thinsp)
619
+ else
620
+ text.gsub!(acronym) do
621
+ expl = $2.to_s; process_escape_html(expl)
622
+ "<acronym title=\"#{expl}\">#{$1}</acronym>"
623
+ end
624
+ end
625
+ end
626
+
627
+ # Обработка знака дюйма, кроме случаев когда он внутри кавычек
628
+ def process_inches(text)
629
+ text.gsub!(/\s([0-9]{1,2}([\.,][0-9]{1,2})?)(\"){1,1}/ui, ' \1' + @inch)
630
+ end
631
+
632
+ def process_plusmin(text)
633
+ text.gsub!(/[^+]\+\-/ui, @plusmn)
634
+ end
635
+
636
+ # Подменяет все юникодные entities в тексте на истинные UTF-8-символы
637
+ def process_raw_output(text)
638
+ # Все глифы
639
+ @glyph.values.each do | entity |
640
+ next unless entity =~ /^&#(\d+);/
641
+ text.gsub!(/#{entity}/, entity_to_raw_utf8(entity))
642
+ end
643
+ end
644
+
645
+ # Конвертирует юникодные entities в UTF-8-codepoints
646
+ def entity_to_raw_utf8(entity)
647
+ entity =~ /^&#(\d+);/
648
+ $1 ? [$1.to_i].pack("U") : entity
649
+ end
650
+
651
+ end #end Gilenson
652
+
653
+ # Выбрасывается если форматтеру задается неизвестная настройка
654
+ class RuTils::Gilenson::UnknownSetting < RuntimeError
655
+ end
656
+
657
+ module RuTils::Gilenson::StringFormatting
658
+ # Форматирует строку с помощью Gilenson::Formatter. Все дополнительные опции передаются форматтеру.
659
+ def gilensize(*args)
660
+ RuTils::Gilenson::Formatter.new(self, args.shift || {}).to_html
661
+ end
662
+
663
+ end
664
+
665
+ Object::String.send(:include, RuTils::Gilenson::StringFormatting)