rutils 0.1.2 → 0.1.3

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.
@@ -1,279 +1,624 @@
1
- class RuTils::Gilenson::New < String #:nodoc:
1
+ module RuTils
2
+ module Gilenson
3
+ # Позволяет возвращать класс форматтера при вызове
4
+ # RuTils::Gilenson.new
5
+ def self.new(*args) #:nodoc:
6
+ RuTils::Gilenson::Formatter.new(*args)
7
+ end
8
+
9
+ # Загружаем "старый" Гиленсон если он будет нужен
10
+ def self.const_missing(const) #:nodoc:
11
+ super(const) unless const == :Obsolete
12
+ require File.dirname(__FILE__) + '/gilenson_port'
13
+ return RuTils::Gilenson::Obsolete
14
+ end
15
+ end
16
+ end
2
17
 
3
- def initialize(*args)
4
- # Задача (вкратце) состоит в том чтобы все ступени разработки развести в отдельные методы
5
- # и тестировать их отдельно друг от друга (а также иметь возможность их по-одному включать и выключать).
6
- # Фильтры, которые начинаются с lift работают с блоком (например - вытащить таги, провести обработку
7
- # текста и вернуть все назад)
8
-
9
- # Фильтры обрабатываются именно в таком порядке. Этот массив стравнивается с настройками, и если настройки
10
- # для конкретного фильтра установлены в false этот фильтр обработан не будет.
11
- # Каждый фильтр должен именоваться process_{filter}, принимать аргументом текст для обработки и возвращать его же!
12
- # После того как фильтр включен в массив order_of_filters и для него написан метод фильтр по лумолчанию включается,
13
- # и его настройку можно поменять с помощью аксессора с соотв. именем. Это делается автоматом.
14
- # Главный обработчик должен сам понимать, использовать ли блок (если метод-делегат начинается с lift_)
15
- # или просто process.
18
+ # ==Что такое Gilenson
19
+ # Обработчик типографских символов в HTML согласно общепринятым правилам. Пока присутствует только в CVS.
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
+ # Если вам нужно получать идентичный Typografica вывод, пользуйтесь RuTils::Gilenson::Obsolete
32
+ # вместо RuTils::Gilenson::Formatter.
33
+ #
34
+ # ==Использование
35
+ # Быстрее всего - через метод ++gilensize++ для любой строковой переменной
36
+ # %{ И вот они таки "приехали"}.gilensize => 'И&#160;вот они&#160;таки &#171;приехали&#187;'
37
+ # Все дополнительные настройки в таком случае передаются форматтеру
38
+ # %{ И вот они таки "приехали"}.gilensize(:laquo=>false) => 'И&#160;вот они&#160;таки "приехали"'
39
+ #
40
+ # Если форматтер надо настроить более тонко, можно использовать его и так:
41
+ # typ = RuTils::Gilenson.new('Эти "так называемые" великие деятели')
42
+ # typ.to_html => 'Эти &#171;так называемые&#187; великие деятели'
43
+ #
44
+ # или как фильтр
45
+ # formatter = RuTils::Gilenson.new
46
+ # formatter.configure(:dash=>true)
47
+ # for string in strings
48
+ # puts formatter.process(string)
49
+ # end
50
+ #
51
+ # ==Настройки
52
+ # Настройки регулируются через методы
53
+ # formatter.dashglue = true
54
+ # или ассоциированным хешем
55
+ # formatter.configure!(:dash=>true, :quotes=>false)
56
+ #
57
+ # Хеш также можно передавать как последний аргумент методам process и to_html,
58
+ # в таком случае настройки будут применены только при этом вызове
59
+ #
60
+ # beautified = formatter.process(my_text, :dash=>true)
61
+ #
62
+ # В параметры можно подставить также ключ :all чтобы временно включить или выключить все фильтры
63
+ #
64
+ # beautified = formatter.process(my_text, :all=>true)
65
+ #
66
+ # Помимо этого можно пользоваться каждым фильтром по отдельности используя метод +apply+
67
+ #
68
+ # Можно менять глифы, которые форматтер использует для подстановок. К примеру,
69
+ # formatter.glyph[:nbsp] = '&nbsp;'
70
+ # заставит форматтер расставлять "традиционные" неразрывные пробелы. Именно это - большая глупость,
71
+ # но другие глифы заменить может быть нужно.
72
+ #
73
+ # ==Настройки форматтера
74
+ # "inches" - преобразовывать дюймы в знак дюйма;
75
+ # "laquo" - кавычки-ёлочки
76
+ # "quotes" - кавычки-английские лапки
77
+ # "dash" - проставлять короткое тире (150)
78
+ # "emdash" - длинное тире двумя минусами (151)
79
+ # "initials" - проставлять тонкие шпации в инициалах
80
+ # "copypaste" - замена непечатных и "специальных" юникодных символов на entities
81
+ # "(c)" - обрабатывать знак копирайта
82
+ # "(r)", "(tm)", "(p)", "+-" - спецсимволы, какие - понятно
83
+ # "degrees" - знак градуса
84
+ # "dashglue", "wordglue" - приклеивание предлогов и дефисов
85
+ # "spacing" - запятые и пробелы, перестановка
86
+ # "phones" - обработка телефонов
87
+ # "html" - при false - запрет использования тагов html
88
+ # "de_nobr" - при true все <nobr/> заменяются на <span class="nobr"/>
89
+ # "raw_output" - (по умолчанию false) - при true вместо entities выводятся UTF-символы
90
+ # "skip_attr" - (по умолчанию false) - при true не отрабатывать типографику в атрибутах тегов (title, alt)
91
+ # "skip_code" - (по умолчанию true) - при true не отрабатывать типографику внутри <code/>, <tt/>, CDATA
16
92
 
17
- # Аксессор само собой генерируется автоматом.
93
+ class RuTils::Gilenson::Formatter
94
+ attr_accessor :glyph
18
95
 
19
- @@order_of_filters = [
20
- :inches,
21
- :dashes,
22
- :emdashes,
23
- :specials,
24
- :spacing,
25
- :dashglue,
26
- :nonbreakables,
27
- :plusmin,
28
- :degrees,
29
- :phones,
30
- :simple_quotes,
31
- :typographer_quotes,
32
- :compound_quotes,
33
- ]
34
-
35
- # Символы, используемые в подстановках. Меняются через substitute_set(subst_name, subst_content)
36
- # Нужно потому как ващето &nbsp; недопустим в XML, равно как и всякие mdash.
37
- @@spec_chars = {
38
- :laquo=>'&laquo;', #left acute
39
- :raquo=>'&raquo;', #right acute
40
- :ndash=>'&ndash;', #en dash
41
- :mdash=>'&mdash;', #en dash
42
- :inch=>'&quot;', #en dash
43
- :nbsp=>'&nbsp;', #non-breakable
44
- }
45
-
46
- @@phonemasks = [[ /([0-9]{4})\-([0-9]{2})\-([0-9]{2}) ([0-9]{2}):([0-9]{2}):([0-9]{2})/,
47
- /([0-9]{4})\-([0-9]{2})\-([0-9]{2})/,
48
- /(\([0-9\+\-]+\)) ?([0-9]{3})\-([0-9]{2})\-([0-9]{2})/,
49
- /(\([0-9\+\-]+\)) ?([0-9]{2})\-([0-9]{2})\-([0-9]{2})/,
50
- /(\([0-9\+\-]+\)) ?([0-9]{3})\-([0-9]{2})/,
51
- /(\([0-9\+\-]+\)) ?([0-9]{2})\-([0-9]{3})/,
52
- /([0-9]{3})\-([0-9]{2})\-([0-9]{2})/,
53
- /([0-9]{2})\-([0-9]{2})\-([0-9]{2})/,
54
- /([0-9]{1})\-([0-9]{2})\-([0-9]{2})/,
55
- /([0-9]{2})\-([0-9]{3})/,
56
- /([0-9]+)\-([0-9]+)/,
57
- ],[
58
- '<nobr>\1&ndash;\2&ndash;\3&nbsp;\4:\5:\6</nobr>',
59
- '<nobr>\1&ndash;\2&ndash;\3</nobr>',
60
- '<nobr>\1&nbsp;\2&ndash;\3&ndash;\4</nobr>',
61
- '<nobr>\1&nbsp;\2&ndash;\3&ndash;\4</nobr>',
62
- '<nobr>\1&nbsp;\2&ndash;\3</nobr>',
63
- '<nobr>\1&nbsp;\2&ndash;\3</nobr>',
64
- '<nobr>\1&ndash;\2&ndash;\3</nobr>',
65
- '<nobr>\1&ndash;\2&ndash;\3</nobr>',
66
- '<nobr>\1&ndash;\2&ndash;\3</nobr>',
67
- '<nobr>\1&ndash;\2</nobr>',
68
- '<nobr>\1&ndash;\2</nobr>'
69
- ]]
70
-
71
- @@glueleft = ['рис.', 'табл.', 'см.', 'им.', 'ул.', 'пер.', 'кв.', 'офис', 'оф.', 'г.']
72
- @@glueright = ['руб.', 'коп.', 'у.е.', 'мин.']
73
-
74
- @@settings = {
75
- "inches" => true, # преобразовывать дюймы в &quot;
76
- "laquo" => true, # кавычки-ёлочки
77
- "farlaquo" => false, # кавычки-ёлочки для фара (знаки "больше-меньше")
78
- "quotes" => true, # кавычки-английские лапки
79
- "dash" => true, # короткое тире (150)
80
- "emdash" => true, # длинное тире двумя минусами (151)
81
- "(c)" => true,
82
- "(r)" => true,
83
- "(tm)" => true,
84
- "(p)" => true,
85
- "+-" => true, # спецсимволы, какие - понятно
86
- "degrees" => true, # знак градуса
87
- "<-->" => true, # отступы $Indent*
88
- "dashglue" => true, "wordglue" => true, # приклеивание предлогов и дефисов
89
- "spacing" => true, # запятые и пробелы, перестановка
90
- "phones" => true, # обработка телефонов
91
- "fixed" => false, # подгон под фиксированную ширину
92
- "html" => false # запрет тагов html
93
- }
94
- # irrelevant - indentation with images
95
- @@indent_a = "<!--indent-->"
96
- @@indent_b = "<!--indent-->"
97
-
98
- @@mark_tag = "\xF0\xF0\xF0\xF0" # Подстановочные маркеры тегов - BOM
99
- @@mark_ignored = "\xFF\xFF\xFF\xFF" # Подстановочные маркеры неизменяемых групп - BOM+ =)
100
-
101
- @@ignore = /notypo/ # regex, который игнорируется. Этим надо воспользоваться для обработки pre и code
96
+ SETTINGS = {
97
+ "inches" => true, # преобразовывать дюймы в знак дюйма;
98
+ "laquo" => true, # кавычки-ёлочки
99
+ "quotes" => true, # кавычки-английские лапки
100
+ "dash" => true, # короткое тире (150)
101
+ "emdash" => true, # длинное тире двумя минусами (151)
102
+ "initials" => true, # тонкие шпации в инициалах
103
+ "copypaste" => false, # замена непечатных и "специальных" юникодных символов на entities
104
+ "(c)" => true, # обрабатывать знак копирайта
105
+ "(r)" => true,
106
+ "(tm)" => true,
107
+ "(p)" => true,
108
+ "+-" => true, # спецсимволы, какие - понятно
109
+ "degrees" => true, # знак градуса
110
+ "dashglue" => true, "wordglue" => true, # приклеивание предлогов и дефисов
111
+ "spacing" => true, # запятые и пробелы, перестановка
112
+ "phones" => true, # обработка телефонов
113
+ "html" => true, # разрешение использования тагов html
114
+ "de_nobr" => false, # при true все <nobr/> заменяются на <span class="nobr"/>
115
+ "raw_output" => false, # выводить UTF-8 вместо entities
116
+ "skip_attr" => false, # при true не отрабатывать типографику в атрибутах тегов
117
+ "skip_code" => true, # при true не отрабатывать типографику внутри <code/>, <tt/>, CDATA
118
+ } #:nodoc:
119
+
120
+ # Глифы, использующиеся в подстановках по-умолчанию
121
+ GLYPHS = {
122
+ :quot => "&#34;", # quotation mark
123
+ :amp => "&#38;", # ampersand
124
+ :apos => "&#39;", # apos
125
+ :gt => "&#62;", # greater-than sign
126
+ :lt => "&#60;", # less-than sign
127
+ :nbsp => "&#160;", # non-breaking space
128
+ :sect => "&#167;", # section sign
129
+ :copy => "&#169;", # copyright sign
130
+ :laquo => "&#171;", # left-pointing double angle quotation mark = left pointing guillemet
131
+ :reg => "&#174;", # registered sign = registered trade mark sign
132
+ :deg => "&#176;", # degree sign
133
+ :plusmn => "&#177;", # plus-minus sign = plus-or-minus sign
134
+ :para => "&#182;", # pilcrow sign = paragraph sign
135
+ :middot => "&#183;", # middle dot = Georgian comma = Greek middle dot
136
+ :raquo => "&#187;", # right-pointing double angle quotation mark = right pointing guillemet
137
+ :ndash => "&#8211;", # en dash
138
+ :mdash => "&#8212;", # em dash
139
+ :lsquo => "&#8216;", # left single quotation mark
140
+ :rsquo => "&#8217;", # right single quotation mark
141
+ :ldquo => "&#8220;", # left double quotation mark
142
+ :rdquo => "&#8221;", # right double quotation mark
143
+ :bdquo => "&#8222;", # double low-9 quotation mark
144
+ :bull => "&#8226;", # bullet = black small circle
145
+ :hellip => "&#8230;", # horizontal ellipsis = three dot leader
146
+ :numero => "&#8470;", # numero
147
+ :trade => "&#8482;", # trade mark sign
148
+ :minus => "&#8722;", # minus sign
149
+ :inch => "&#8243;", # inch/second sign (u0x2033) (не путать с кавычками!)
150
+ :thinsp => "&#8201;", # полукруглая шпация (тонкий пробел)
151
+ :nob_open => '<nobr>', # открывающий блок без переноса слов
152
+ :nob_close => '</nobr>', # открывающий блок без переноса слов
153
+ } #:nodoc:
154
+
155
+ # Нормальные "типографские" символы в UTF-виде. Браузерами обрабатываются плохонько, поэтому
156
+ # лучше заменять их на entities.
157
+ VERBATIM_GLYPHS = {
158
+ ' ' => :nbsp,# alt+0160 (NBSP here)
159
+ '«' => :laquo,
160
+ '»' => :raquo,
161
+ '§' => :sect,
162
+ '©' => :copy,
163
+ '®' => :reg,
164
+ '°' => :deg,
165
+ '±' => :plusmn,
166
+ '¶' => :para,
167
+ '·' => :middot,
168
+ '–' => :ndash,
169
+ '—' => :mdash,
170
+ '‘' => :lsquo,
171
+ '’' => :rsquo,
172
+ '“' => :ldquo,
173
+ '”' => :rdquo,
174
+ '„' => :bdquo,
175
+ '•' => :bull,
176
+ '…' => :hellip,
177
+ '№' => :numero,
178
+ '™' => :trade,
179
+ '−' => :minus,
180
+ ' ' => :thinsp,
181
+ '″' => :inch,
182
+ } #:nodoc:
183
+
184
+ # Для маркера мы применяем invalid UTF-sequence чтобы его НЕЛЬЗЯ было перепутать с частью
185
+ # любого другого мультибайтного глифа. Thanks to huNter.
186
+ REPLACEMENT_MARKER = '\xF0\xF0\xF0\xF0' #:nodoc:
102
187
 
103
- self.methods.each do | m |
104
- next unless m.include?("process_")
105
- raise NoMethodError, "No hook for " + m unless @@order_of_filters.include?(m.gsub(/process_/, '').to_sym)
188
+ # Кто придумал &#147;? Не учите людей плохому...
189
+ # Привет А.Лебедеву http://www.artlebedev.ru/kovodstvo/62/
190
+ # Используем символы, потом берем по символам из glyphs форматтера.
191
+ # Молодец mash!
192
+ FORBIDDEN_NUMERIC_ENTITIES = {
193
+ '132' => :bdquo,
194
+ '133' => :hellip,
195
+ '146' => :apos,
196
+ '147' => :ldquo,
197
+ '148' => :rdquo,
198
+ '149' => :bull,
199
+ '150' => :ndash,
200
+ '151' => :mdash,
201
+ '153' => :trade,
202
+ } #:nodoc:
203
+
204
+ PROTECTED_SETTINGS = [ :raw_output ] #:nodoc:
205
+
206
+ def initialize(*args)
207
+ @_text = args[0].is_a?(String) ? args[0] : ''
208
+ setup_default_settings!
209
+ accept_configuration_arguments!(args.last) if args.last.is_a?(Hash)
210
+ end
211
+
212
+ # Настраивает форматтер ассоциированным хешем
213
+ # formatter.configure!(:dash=>true, :wordglue=>false)
214
+ def configure!(*config)
215
+ accept_configuration_arguments!(config.last) if config.last.is_a?(Hash)
106
216
  end
217
+
218
+ alias :configure :configure! #Дружественный API
107
219
 
108
- @@order_of_filters.each do |filter|
109
- raise NoMethodError, "No process method for " + filter unless self.methods.include?("process_#{filter}".to_sym)
220
+ # Неизвестные методы - настройки. С = - установка ключа, без - получение значения
221
+ def method_missing(meth, *args) #:nodoc:
222
+ setting = meth.to_s.gsub(/=$/, '')
223
+ super(meth, *args) unless @settings.has_key?(setting) #this will pop the exception if we have no such setting
224
+
225
+ return (@settings[setting] = args[0])
110
226
  end
111
227
 
112
- super(*args)
228
+ # Обрабатывает text_to_process с сохранением настроек, присвоенных обьекту-форматтеру
229
+ # Дополнительные аргументы передаются как параметры форматтера и не сохраняются после прогона.
230
+ def process(text_to_process, *args)
231
+ @_text = text_to_process
232
+ if args.last.is_a?(Hash)
233
+ with_configuration(args.last) { self.to_html }
234
+ else
235
+ self.to_html
236
+ end
237
+ end
113
238
 
114
- end
239
+ # Обрабатывает текст, присвоенный форматтеру при создании и возвращает результат обработки.
240
+ def to_html()
241
+ return '' unless @_text
242
+
243
+ text = @_text.strip
115
244
 
116
-
117
- def to_html(*opts)
118
- text = self.to_s.clone
119
- lift_tags(text) do | text |
120
- # lift_ignored(text) do |text|
121
- for filter in @@order_of_filters
122
- raise "UnknownFilter #process_#{filter} in filterlist!" unless self.respond_to?("process_#{filter}".to_sym)
123
- self.send("process_#{filter}".to_sym, text) # if @settings[filter.to_sym] # вызываем конкретный фильтр
124
- end
125
- # end
245
+ # -4. запрет тагов html
246
+ process_escape_html(text) unless @settings["html"]
247
+
248
+ # -3. Никогда (вы слышите?!) не пущать лабуду &#not_correct_number;
249
+ FORBIDDEN_NUMERIC_ENTITIES.dup.each_pair do | key, rep |
250
+ text.gsub!(/&##{key};/, glyph[rep])
251
+ end
252
+
253
+ # -2. Чистим copy&paste
254
+ process_copy_paste_clearing(text) if @settings['copypaste']
255
+
256
+ # -1. Замена &entity_name; на входе ('&nbsp;' => '&#160;' и т.д.)
257
+ process_html_entities(text)
258
+
259
+ # 0. Вырезаем таги
260
+ tags = lift_ignored_elements(text) if @skip_tags
261
+
262
+ # 1. Запятые и пробелы
263
+ process_spacing(text) if @settings["spacing"]
264
+
265
+ # 3. Спецсимволы
266
+ # 0. дюймы с цифрами
267
+ # заменено на инчи
268
+ process_inches(text) if @settings["inches"]
269
+
270
+ # 1. лапки
271
+ process_quotes(text) if @settings["quotes"]
272
+
273
+ # 2. ёлочки
274
+ process_laquo(text) if @settings["laquo"]
275
+
276
+ # 2b. одновременно ёлочки и лапки
277
+ process_compound_quotes(text) if (@settings["quotes"] && @settings["laquo"])
278
+
279
+ # 3. тире
280
+ process_dash(text) if @settings["dash"]
281
+
282
+ # 3a. тире длинное
283
+ process_emdash(text) if @settings["emdash"]
284
+
285
+ # 5. +/-
286
+ process_plusmin(text) if @settings["+-"]
287
+
288
+ # 5a. 12^C
289
+ process_degrees(text) if @settings["degrees"]
290
+
291
+ # 6. телефоны
292
+ process_phones(text) if @settings["phones"]
293
+
294
+ # 7. Короткие слова и &nbsp;
295
+ process_wordglue(text) if @settings["wordglue"]
296
+
297
+ # 8. Склейка ласт. Тьфу! дефисов.
298
+ process_dashglue(text) if @settings["dashglue"]
299
+
300
+ # 8a. Инициалы
301
+ process_initials(text) if @settings['initials']
302
+
303
+ # 8b. Троеточия
304
+ process_ellipsises(text) if @settings["wordglue"]
305
+
306
+ # БЕСКОНЕЧНОСТЬ. Вставляем таги обратно.
307
+ reinsert_fragments(text, tags) if @skip_tags
308
+
309
+ # фуф, закончили.
310
+ process_span_instead_of_nobr(text) if @settings["de_nobr"]
311
+
312
+ # заменяем entities на истинные символы
313
+ process_raw_output(text) if @settings["raw_output"]
314
+
315
+ text.strip
126
316
  end
127
- text
128
- end
129
-
130
- # Вытаскивает теги из текста, выполняет переданный блок и возвращает теги на место.
131
- # Теги в процессе заменяются на специальный маркер
132
- def lift_tags(text, marker="\xF0\xF0\xF0\xF0", &block)
133
-
134
- # Выцепляем таги
135
- # re = /<\/?[a-z0-9]+("+ # имя тага
136
- # "\s+("+ # повторяющая конструкция: хотя бы один разделитель и тельце
137
- # "[a-z]+("+ # атрибут из букв, за которым может стоять знак равенства и потом
138
- # "=((\'[^\']*\')|(\"[^\"]*\")|([0-9@\-_a-z:\/?&=\.]+))"+ #
139
- # ")?"+
140
- # ")?"+
141
- # ")*\/?>|\xA2\xA2[^\n]*?==/i;
142
-
143
- re = /(<\/?[a-z0-9]+(\s+([a-z]+(=((\'[^\']*\')|(\"[^\"]*\")|([0-9@\-_a-z:\/?&=\.]+)))?)?)*\/?>)/ui
144
-
145
- tags = text.scan(re).inject([]) { | ar, match | ar << match[0] }
146
- text.gsub!(re, "\xF0\xF0\xF0\xF0") #маркер тега
147
-
148
- yield(text, marker) if block_given? #делаем все что надо сделать без тегов
317
+
149
318
 
150
- tags.each { | tag | text.sub!(marker, tag) } # Вставляем таги обратно.
319
+ # Применяет отдельный фильтр к text и возвращает результат. Например:
320
+ # formatter.apply(:wordglue, "Вот так") => "Вот&#160;так"
321
+ # Удобно применять когда вам нужно задействовать отдельный фильтр Гиленсона, но не нужна остальная механика
322
+ # Последний аргумент определяет, нужно ли при применении фильтра сохранить в неприкосновенности таги и другие
323
+ # игнорируемые фрагменты текста (по умолчанию они сохраняются).
324
+ def apply(filter, text, lift_ignored_elements = true)
325
+ copy = text.dup
326
+ unless lift_ignored_elements
327
+ self.send("process_#{filter}".to_sym, copy)
328
+ else
329
+ lifting_fragments(copy) { self.send("process_#{filter}".to_sym, copy) }
330
+ end
331
+ copy
332
+ end
333
+
334
+ private
335
+
336
+ def setup_default_settings!
337
+ @skip_tags = true;
338
+ @ignore = /notypo/ # regex, который игнорируется. Этим надо воспользоваться для обработки pre и code
151
339
 
152
- end
340
+ @glueleft = ['рис.', 'табл.', 'см.', 'им.', 'ул.', 'пер.', 'кв.', 'офис', 'оф.', 'г.']
341
+ @glueright = ['руб.', 'коп.', 'у.е.', 'мин.']
153
342
 
154
- # Выцепляет игнорированные символы, выполняет блок с текстом
155
- # без этих символов а затем вставляет их на место
156
- def lift_ignored(text, marker = "\xFF\xFF\xFF\xFF", &block)
157
- ignored = text.scan(@ignore)
158
- text.gsub!(@ignore, marker)
159
-
160
- # обрабатываем текст
161
- yield(text, marker) if block_given?
162
-
163
- # возвращаем игнорированные символы
164
- ignored.each { | tag | text.sub!(marker, tag) }
165
- end
343
+ # Установки можно менять в каждом экземпляре
344
+ @settings = SETTINGS.dup
166
345
 
167
- # Кавычки - лапки
168
- def process_simple_quotes(text)
169
- text.gsub!( /\"\"/ui, "&quot;&quot;")
170
- text.gsub!( /\"\.\"/ui, "&quot;.&quot;")
171
- _text = '""';
172
- while _text != text do
173
- _text = text
174
- text.gsub!( /(^|\s|\xFF\xFF\xFF\xFF|\xF0\xF0\xF0\xF0|>)\"([0-9A-Za-z\'\!\s\.\?\,\-\&\;\:\_\xF0\xF0\xF0\xF0\xFF\xFF\xFF\xFF]+(\"|&#148;))/ui, '\1&#147;\2')
175
- #this doesnt work in-place. somehow.
176
- text.replace text.gsub( /(\&\#147\;([A-Za-z0-9\'\!\s\.\?\,\-\&\;\:\xF0\xF0\xF0\xF0\xFF\xFF\xFF\xFF\_]*).*[A-Za-z0-9][\xF0\xF0\xF0\xF0\xFF\xFF\xFF\xFF\?\.\!\,]*)\"/ui, '\1&#148;')
346
+ @mark_tag = REPLACEMENT_MARKER
347
+ # Глифы можено подменять в экземпляре форматтера поэтому копируем их из константы
348
+ @glyph = GLYPHS.dup
349
+
350
+ @phonemasks = [[ /([0-9]{4})\-([0-9]{2})\-([0-9]{2}) ([0-9]{2}):([0-9]{2}):([0-9]{2})/,
351
+ /([0-9]{4})\-([0-9]{2})\-([0-9]{2})/,
352
+ /(\([0-9\+\-]+\)) ?([0-9]{3})\-([0-9]{2})\-([0-9]{2})/,
353
+ /(\([0-9\+\-]+\)) ?([0-9]{2})\-([0-9]{2})\-([0-9]{2})/,
354
+ /(\([0-9\+\-]+\)) ?([0-9]{3})\-([0-9]{2})/,
355
+ /(\([0-9\+\-]+\)) ?([0-9]{2})\-([0-9]{3})/,
356
+ /([0-9]{3})\-([0-9]{2})\-([0-9]{2})/,
357
+ /([0-9]{2})\-([0-9]{2})\-([0-9]{2})/,
358
+ /([0-9]{1})\-([0-9]{2})\-([0-9]{2})/,
359
+ /([0-9]{2})\-([0-9]{3})/,
360
+ /([0-9]+)\-([0-9]+)/,
361
+ ],[
362
+ ':nob_open\1:ndash\2:ndash\3:nbsp\4:\5:\6:nob_close',
363
+ ':nob_open\1:ndash\2:ndash\3:nob_close',
364
+ ':nob_open\1:nbsp\2:ndash\3:ndash\4:nob_close',
365
+ ':nob_open\1:nbsp\2:ndash\3:ndash\4:nob_close',
366
+ ':nob_open\1:nbsp\2:ndash\3:nob_close',
367
+ ':nob_open\1:nbsp\2:ndash\3:nob_close',
368
+ ':nob_open\1:ndash\2:ndash\3:nob_close',
369
+ ':nob_open\1:ndash\2:ndash\3:nob_close',
370
+ ':nob_open\1:ndash\2:ndash\3:nob_close',
371
+ ':nob_open\1:ndash\2:nob_close',
372
+ ':nob_open\1:ndash\2:nob_close'
373
+ ]]
374
+ end
375
+
376
+ # Позволяет получить процедуру, при вызове возвращающую значение глифа
377
+ def lookup(glyph_to_lookup)
378
+ gil = self
379
+ return Proc.new { gil.glyph[glyph_to_lookup] }
177
380
  end
178
- end
179
-
180
- # Кавычки - елочки
181
- def process_typographer_quotes(text)
182
- # 2. ёлочки
183
- text.gsub!( /\"\"/ui, "&quot;&quot;");
184
- text.gsub!( /(^|\s|\xFF\xFF\xFF\xFF|\xF0\xF0\xF0\xF0|>|\()\"((\xFF\xFF\xFF\xFF|\xF0\xF0\xF0\xF0)*[~0-9ёЁA-Za-zА-Яа-я\-:\/\.])/ui, "\\1&laquo;\\2");
185
- # nb: wacko only regexp follows:
186
- text.gsub!( /(^|\s|\xFF\xFF\xFF\xFF|\xF0\xF0\xF0\xF0|>|\()\"((\xFF\xFF\xFF\xFF|\xF0\xF0\xF0\xF0|\/&nbsp;|\/|\!)*[~0-9ёЁA-Za-zА-Яа-я\-:\/\.])/ui, "\\1&laquo;\\2")
187
- _text = "\"\"";
188
- while (_text != text) do
189
- _text = text;
190
- text.gsub!( /(\&laquo\;([^\"]*)[ёЁA-Za-zА-Яа-я0-9\.\-:\/](\xFF\xFF\xFF\xFF|\xF0\xF0\xF0\xF0)*)\"/sui, "\\1&raquo;")
191
- # nb: wacko only regexps follows:
192
- text.gsub!( /(\&laquo\;([^\"]*)[ёЁA-Za-zА-Яа-я0-9\.\-:\/](\xFF\xFF\xFF\xFF|\xF0\xF0\xF0\xF0)*\?(\xFF\xFF\xFF\xFF|\xF0\xF0\xF0\xF0)*)\"/sui, "\\1&raquo;")
193
- text.gsub!( /(\&laquo\;([^\"]*)[ёЁA-Za-zА-Яа-я0-9\.\-:\/](\xFF\xFF\xFF\xFF|\xF0\xF0\xF0\xF0|\/|\!)*)\"/sui, "\\1&raquo;")
194
- end
195
- end
196
-
197
- # Cложные кавычки
198
- def process_compound_quotes(text)
199
- text.gsub!(/(\&\#147\;(([A-Za-z0-9'!\.?,\-&;:]|\s|\xF0\xF0\xF0\xF0|\xFF\xFF\xFF\xFF)*)&laquo;(.*)&raquo;)&raquo;/ui,"\\1&#148;");
200
- end
201
381
 
202
- # Обрабатывает короткое тире
203
- def process_dashes(text)
204
- text.gsub!( /(\s|;)\-(\s)/ui, "\\1&ndash;\\2")
205
- end
206
-
207
- # Обрабатывает длинные тире
208
- def process_emdashes(text)
209
- text.gsub!( /(\s|;)\-\-(\s)/ui, "\\1&mdash;\\2")
210
- end
382
+ # Подставляет "символы" (двоеточие + имя глифа) на нужное значение глифа заданное в данном форматтере
383
+ def substitute_glyphs_in_string(str)
384
+ re = str.dup
385
+ @glyph.each_pair do | key, subst |
386
+ re.gsub!(":#{key.to_s}", subst)
387
+ end
388
+ re
389
+ end
211
390
 
212
- # Обрабатывает знаки копирайта, торговой марки и т.д.
213
- def process_specials(text)
214
- # 4. (с)
215
- text.gsub!(/\([сСcC]\)((?=\w)|(?=\s[0-9]+))/u, "&copy;")
216
- # 4a. (r)
217
- text.gsub!( /\(r\)/ui, "<sup>&#174;</sup>")
218
-
219
- # 4b. (tm)
220
- text.gsub!( /\(tm\)|\(тм\)/ui, "&#153;")
221
- # 4c. (p)
222
- text.gsub!( /\(p\)/ui, "&#167;")
223
- end
391
+ # Выполняет блок, временно включая настройки переданные в +hash+
392
+ def with_configuration(hash, &block)
393
+ old_settings, old_glyphs = @settings.dup, @glyph.dup
394
+ accept_configuration_arguments!(hash)
395
+ txt = yield
396
+ @settings, @glyph = old_settings, old_glyphs
224
397
 
225
- # Склейка дефисоов
226
- def process_dashglue(text)
227
- text.gsub!( /([a-zа-яА-Я0-9]+(\-[a-zа-яА-Я0-9]+)+)/ui, '<nobr>\1</nobr>')
228
- end
229
-
230
- # Запятые и пробелы
231
- def process_spacing(text)
232
- text.gsub!( /(\s*)([,]*)/sui, "\\2\\1");
233
- text.gsub!( /(\s*)([\.?!]*)(\s*[ЁА-ЯA-Z])/su, "\\2\\1\\3");
234
- end
235
-
236
- # Неразрывные пробелы - пока глючит страшным образом
237
- def process_nonbreakables(text)
238
- text.replace " " + text + " ";
239
- _text = " " + text + " ";
240
- until _text == text
241
- _text.replace text.clone
242
- text.gsub!( /(\s+)([a-zа-яА-Я]{1,2})(\s+)([^\\s$])/ui, '\1\2&nbsp;\4')
243
- text.gsub!( /(\s+)([a-zа-яА-Я]{3})(\s+)([^\\s$])/ui, '\1\2&nbsp;\4')
398
+ return txt
399
+ end
400
+
401
+ def accept_configuration_arguments!(args_hash)
402
+
403
+ # Специальный случай - :all=>true|false
404
+ if args_hash.has_key?(:all)
405
+ if args_hash[:all]
406
+ @settings.each_pair {|k, v| @settings[k] = true unless PROTECTED_SETTINGS.include?(k.to_sym)}
407
+ else
408
+ @settings.each_pair {|k, v| @settings[k] = false unless PROTECTED_SETTINGS.include?(k.to_sym)}
409
+ end
410
+ else
411
+
412
+ # Кинуть ошибку если настройка нам неизвестна
413
+ unknown_settings = args_hash.keys.collect{|k|k.to_s} - @settings.keys.collect { |k| k.to_s }
414
+ raise RuTils::Gilenson::UnknownSetting, unknown_settings unless unknown_settings.empty?
415
+
416
+ args_hash.each_pair do | key, value |
417
+ @settings[key.to_s] = (value ? true : false)
418
+ end
419
+ end
244
420
  end
245
421
 
246
- for i in @glueleft
247
- text.gsub!( /(\s)(#{i})(\s+)/sui, '\1\2&nbsp;')
422
+ # Вынимает игнорируемые фрагменты и заменяет их маркером, выполняет переданный блок и вставляет вынутое на место
423
+ def lifting_fragments(text, &block)
424
+ lifted = lift_ignored_elements(text)
425
+ yield
426
+ reinsert_fragments(text, lifted)
248
427
  end
428
+
429
+ #Вынимает фрагменты из текста и возвращает массив с фрагментами
430
+ def lift_ignored_elements(text)
431
+ # re = /<\/?[a-z0-9]+("+ # имя тага
432
+ # "\s+("+ # повторяющая конструкция: хотя бы один разделитель и тельце
433
+ # "[a-z]+("+ # атрибут из букв, за которым может стоять знак равенства и потом
434
+ # "=((\'[^\']*\')|(\"[^\"]*\")|([0-9@\-_a-z:\/?&=\.]+))"+ #
435
+ # ")?"+
436
+ # ")?"+
437
+ # ")*\/?>|\xA2\xA2[^\n]*?==/i;
249
438
 
250
- for i in @glueright
251
- text.gsub!( /(\s)(#{i})(\s+)/sui, '&nbsp;\2\3')
439
+ re_skipcode = '((<(code|tt)[ >](.*?)<\/(code|tt)>)|(<!\[CDATA\[(.*?)\]\]>))|' if @settings['skip_code']
440
+ re = /(#{re_skipcode}<\/?[a-z0-9]+(\s+([a-z]+(=((\'[^\']*\')|(\"[^\"]*\")|([0-9@\-_a-z:\/?&=\.]+)))?)?)*\/?>)/uim
441
+ tags = text.scan(re).map{ |tag| tag[0] } # первая группа!
442
+ text.gsub!(re, @mark_tag) #маркер тега, мы используем Invalid UTF-sequence для него
443
+ return tags
252
444
  end
253
445
 
254
- end
446
+ def reinsert_fragments(text, fragments)
447
+ fragments.each { |fragment|
448
+ fragment.gsub!(/ (href|src|data)=((?:(\')([^\']*)(\'))|(?:(\")([^\"]*)(\")))/uim) {
449
+ " #{$1}=" + $2.gsub(/&(?!(#0*38)|(amp);)/, self.glyph[:amp])
450
+ } # unless @settings['raw_output'] -- делать это надо всегда (mash)
451
+
452
+ fragment.gsub!(/ (title|alt)=((?:(\')([^\']*)(\'))|(?:(\")([^\"]*)(\")))/uim) {
453
+ " #{$1}=#{$3}" + self.process($4.to_s) + "#{$5}#{$6}" + self.process($7.to_s) + "#{$8}"
454
+ } unless @settings['skip_attr']
455
+ text.sub!(@mark_tag, fragment)
456
+ }
457
+ end
255
458
 
256
- # Знак дюйма
257
- def process_inches(text)
258
- text.gsub!(/\s([0-9]{1,2}([\.,][0-9]{1,2})?)\"/ui, ' \1&quot;') if @settings["inches"]
259
- end
260
-
261
- # Обрабатывает знак +/-
262
- def process_plusmin(text)
263
- text.gsub!(/\+\-/ui, "&#177;") if @settings["+-"]
264
- end
459
+ ### Имплементации фильтров
460
+ def process_html_entities(text)
461
+ self.glyph.each { |key, value| text.gsub!(/&#{key};/, value)}
462
+ end
463
+
464
+ def process_initials(text)
465
+ initials = /([А-Я])[\.]*?[\s]*?([А-Я])[\.]*[\s]*?([А-Я])([а-я])/u
466
+ replacement = substitute_glyphs_in_string('\1.\2.:thinsp\3\4')
467
+ text.gsub!(initials, replacement)
468
+ end
265
469
 
266
- # Обрабатывает телефоны
267
- def process_phones(text)
268
- @phonemasks[0].each_with_index do |regex, i|
269
- text.gsub!(regex, @phonemasks[1][i])
270
- end
470
+ def process_copy_paste_clearing(text)
471
+ VERBATIM_GLYPHS.each {|key,value| text.gsub!(/#{key}/, glyph[value]) }
472
+ end
473
+
474
+ def process_spacing(text)
475
+ text.gsub!( /(\s*)([,]*)/sui, '\2\1');
476
+ text.gsub!( /(\s*)([\.?!]*)(\s*[ЁА-ЯA-Z])/su, '\2\1\3');
477
+ end
478
+
479
+ def process_dashglue(text)
480
+ text.gsub!( /([a-zа-яА-Я0-9]+(\-[a-zа-яА-Я0-9]+)+)/ui, '<nobr>\1</nobr>')
481
+ end
482
+
483
+ def process_escape_html(text)
484
+ text.gsub!(/&/, self.glyph[:amp])
485
+ text.gsub!(/</, self.glyph[:lt])
486
+ text.gsub!(/>/, self.glyph[:gt])
487
+ end
488
+
489
+ def process_span_instead_of_nobr(text)
490
+ text.gsub!(/<nobr>/, '<span class="nobr">')
491
+ text.gsub!(/<\/nobr>/, '</span>')
492
+ end
493
+
494
+ def process_dash(text)
495
+ text.gsub!( /(\s|;)\-(\s)/ui, '\1'+self.glyph[:ndash]+'\2')
496
+ end
497
+
498
+ def process_emdash(text)
499
+ text.gsub!( /(\s|;)\-\-(\s)/ui, '\1'+self.glyph[:mdash]+'\2')
500
+ # 4. (с)
501
+ text.gsub!(/\([сСcC]\)((?=\w)|(?=\s[0-9]+))/u, self.glyph[:copy]) if @settings["(c)"]
502
+ # 4a. (r)
503
+ text.gsub!( /\(r\)/ui, '<sup>'+self.glyph[:reg]+'</sup>') if @settings["(r)"]
504
+
505
+ # 4b. (tm)
506
+ text.gsub!( /\(tm\)|\(тм\)/ui, self.glyph[:trade]) if @settings["(tm)"]
507
+ # 4c. (p)
508
+ text.gsub!( /\(p\)/ui, self.glyph[:sect]) if @settings["(p)"]
509
+ end
510
+
511
+ def process_ellipsises(text)
512
+ text.gsub!( '...', self.glyph[:hellip])
513
+ end
514
+
515
+ def process_laquo(text)
516
+ text.gsub!( /\"\"/ui, self.glyph[:quot]*2);
517
+ text.gsub!( /(^|\s|#{@mark_tag}|>|\()\"((#{@mark_tag})*[~0-9ёЁA-Za-zА-Яа-я\-:\/\.])/ui, '\1'+self.glyph[:laquo]+'\2');
518
+ # nb: wacko only regexp follows:
519
+ # text.gsub!( /(^|\s|#{@mark_tag}|>|\()\"((#{#{@mark_tag}|\/#{self.glyph[:nbsp]}|\/|\!)*[~0-9ёЁA-Za-zА-Яа-я\-:\/\.])/ui, '\1'+self.glyph[:laquo]+'\2')
520
+ _text = '""';
521
+ until _text == text do
522
+ _text = text;
523
+ text.gsub!( /(#{self.glyph[:laquo]}([^\"]*)[ёЁA-Za-zА-Яа-я0-9\.\-:\/\?\!](#{@mark_tag})*)\"/sui, '\1'+self.glyph[:raquo])
524
+ # nb: wacko only regexps follows:
525
+ #text.gsub!( /(#{self.glyph[:laquo]}([^\"]*)[ёЁA-Za-zА-Яа-я0-9\.\-:\/](#{@mark_tag})*\?(#{#{@mark_tag})*)\"/sui, '\1'+self.glyph[:raquo])
526
+ # text.gsub!( /(#{self.glyph[:raquo]}([^\"]*)[ёЁA-Za-zА-Яа-я0-9\.\-:\/](#{@mark_tag}|\/|\!)*)\"/sui, '\1'+self.glyph[:raquo])
527
+ end
528
+ end
529
+
530
+ def process_quotes(text)
531
+ text.gsub!( /\"\"/ui, self.glyph[:quot]*2)
532
+ text.gsub!( /\"\.\"/ui, self.glyph[:quot]+"."+self.glyph[:quot])
533
+ _text = '""';
534
+ until _text == text do
535
+ _text = text.dup
536
+ text.gsub!( /(^|\s|#{@mark_tag}|>)\"([0-9A-Za-z\'\!\s\.\?\,\-\&\;\:\_#{@mark_tag}]+(\"|#{self.glyph[:rdquo]}))/ui, '\1'+self.glyph[:ldquo]+'\2')
537
+ #this doesnt work in-place. somehow.
538
+ text.gsub!( /(#{self.glyph[:ldquo]}([A-Za-z0-9\'\!\s\.\?\,\-\&\;\:#{@mark_tag}\_]*).*[A-Za-z0-9][#{@mark_tag}\?\.\!\,]*)\"/ui, '\1'+self.glyph[:rdquo])
539
+ end
540
+ end
541
+
542
+ def process_compound_quotes(text)
543
+ text.gsub!(/(#{self.glyph[:ldquo]}(([A-Za-z0-9'!\.?,\-&;:]|\s|#{@mark_tag})*)#{self.glyph[:laquo]}(.*)#{self.glyph[:raquo]})#{self.glyph[:raquo]}/ui,'\1'+self.glyph[:rdquo]);
544
+ end
545
+
546
+ def process_degrees(text)
547
+ text.gsub!( /-([0-9])+\^([FCС])/, self.glyph[:ndash]+'\1'+self.glyph[:deg]+'\2') #deg
548
+ text.gsub!( /\+([0-9])+\^([FCС])/, '+\1'+self.glyph[:deg]+'\2')
549
+ text.gsub!( /\^([FCС])/, self.glyph[:deg]+'\1')
550
+ end
551
+
552
+ def process_wordglue(text)
553
+ text.replace(" " + text + " ")
554
+ _text = " " + text + " "
555
+
556
+ until _text == text
557
+ _text = text
558
+ text.gsub!( /(\s+)([a-zа-яА-Я]{1,2})(\s+)([^\\s$])/ui, '\1\2'+self.glyph[:nbsp]+'\4')
559
+ text.gsub!( /(\s+)([a-zа-яА-Я]{3})(\s+)([^\\s$])/ui, '\1\2'+self.glyph[:nbsp]+'\4')
560
+ end
561
+
562
+ text.gsub!(/(\s+)([a-zа-яА-Я]{1,2}[\)\]\!\?,\.;]{0,3}\s$)/ui, self.glyph[:nbsp]+'\2')
563
+
564
+ for i in @glueleft
565
+ text.gsub!( /(\s)(#{i})(\s+)/sui, '\1\2' + self.glyph[:nbsp])
566
+ end
567
+
568
+ for i in @glueright
569
+ text.gsub!( /(\s)(#{i})(\s+)/sui, self.glyph[:nbsp]+'\2\3')
570
+ end
571
+
572
+ text.strip!
573
+ end
574
+
575
+ def process_phones(text)
576
+ @phonemasks[0].each_with_index do |pattern, i|
577
+ replacement = substitute_glyphs_in_string(@phonemasks[1][i])
578
+ text.gsub!(pattern, replacement)
579
+ end
580
+ end
581
+
582
+ def process_inches(text)
583
+ text.gsub!(/\s([0-9]{1,2}([\.,][0-9]{1,2})?)\"/ui, ' \1'+self.glyph[:inch])
584
+ end
585
+
586
+ def process_plusmin(text)
587
+ text.gsub!(/[^+]\+\-/ui, self.glyph[:plusmn])
588
+ end
589
+
590
+ # Подменяет все юникодные entities в тексте на истинные UTF-8-символы
591
+ def process_raw_output(text)
592
+ # Все глифы
593
+ @glyph.values.each do | entity |
594
+ next unless entity =~ /^&#(\d+);/
595
+
596
+ text.gsub!(/#{entity}/, entity_to_raw_utf8(entity))
597
+ end
598
+ end
599
+
600
+ # Конвертирует юникодные entities в UTF-8-codepoints
601
+ def entity_to_raw_utf8(entity)
602
+ entity =~ /^&#(\d+);/
603
+ $1 ? [$1.to_i].pack("U") : entity
604
+ end
605
+ end #end Gilenson
606
+
607
+ class RuTils::Gilenson::UnknownSetting < RuntimeError
608
+ end
609
+
610
+ module RuTils::Gilenson::StringFormatting #:nodoc:
611
+ # Форматирует строку с помощью Gilenson::Formatter. Все дополнительные опции передаются форматтеру.
612
+ def gilensize(*args)
613
+ opts = args.last.is_a?(Hash) ? args.last : {}
614
+ RuTils::Gilenson::Formatter.new(self, *opts).to_html
271
615
  end
272
616
 
273
- # Обрабатывает знак градуса, набранный как caret
274
- def process_degrees(text)
275
- text.gsub!( /-([0-9])+\^([FCС])/, '&ndash;\1&#176\2')
276
- text.gsub!( /\+([0-9])+\^([FCС])/, "+\\1&#176\\2")
277
- text.gsub!( /\^([FCС])/, "&#176\\1")
617
+ # Форматирует строку с помощью Gilenson::Obsolete. Всe дополнительные опции передаются форматтеру.
618
+ def o_gilensize(*args)
619
+ opts = args.last.is_a?(Hash) ? args.last : {}
620
+ RuTils::Gilenson::Obsolete.new(self, *opts).to_html
278
621
  end
279
- end
622
+ end
623
+
624
+ Object::String.send(:include, RuTils::Gilenson::StringFormatting)