tml 5.6.5 → 5.7.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/tml.rb +4 -2
- data/lib/tml/application.rb +3 -1
- data/lib/tml/config.rb +28 -0
- data/lib/tml/language.rb +8 -2
- data/lib/tml/tokenizers/data.rb +4 -2
- data/lib/tml/tokenizers/decoration.rb +10 -67
- data/lib/tml/tokenizers/dom.rb +1 -1
- data/lib/tml/tokenizers/x_message.rb +490 -0
- data/lib/tml/tokens/data.rb +14 -1
- data/lib/tml/tokens/decoration.rb +112 -0
- data/lib/tml/tokens/map.rb +83 -0
- data/lib/tml/tokens/transform.rb +0 -1
- data/lib/tml/tokens/x_message/choice.rb +61 -0
- data/lib/tml/tokens/x_message/data.rb +54 -0
- data/lib/tml/tokens/x_message/decoration.rb +101 -0
- data/lib/tml/tokens/x_message/map.rb +54 -0
- data/lib/tml/translation_key.rb +8 -1
- data/lib/tml/utils.rb +1 -1
- data/lib/tml/version.rb +1 -1
- metadata +9 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a26bbf3c59deca21956a94bc33d811377797034e
|
4
|
+
data.tar.gz: fdd19956dcf43dc02228c8c43ea8687a8eb47d34
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f5277433d7a88c672d46f9c214add49e06bb52752fd1e9f23b06f8c1fad3f0db9c40723c3d159078644b59ecfd8f6d372c7b8665f8e6781404788cf19b7ffd7c
|
7
|
+
data.tar.gz: 3e31327a1b93feffc6f86e1d974c0ba64e0a90aac225a154d18eb8c7de67316f1bf10438db03a88e40fe47c4a38fca0bbf6779e4ede7a5c77f8c70855e4755b0
|
data/lib/tml.rb
CHANGED
@@ -32,7 +32,9 @@
|
|
32
32
|
|
33
33
|
module Tml
|
34
34
|
module Api end
|
35
|
-
module Tokens
|
35
|
+
module Tokens
|
36
|
+
module XMessage end
|
37
|
+
end
|
36
38
|
module Tokenizers end
|
37
39
|
module Rules end
|
38
40
|
module Decorators end
|
@@ -64,7 +66,7 @@ module Tml
|
|
64
66
|
end
|
65
67
|
end
|
66
68
|
|
67
|
-
%w(tml/base.rb tml tml/api tml/rules_engine tml/tokens tml/tokenizers tml/decorators tml/cache_adapters tml/cache tml/ext).each do |f|
|
69
|
+
%w(tml/base.rb tml tml/api tml/rules_engine tml/tokens tml/tokens/x_message tml/tokenizers tml/decorators tml/cache_adapters tml/cache tml/ext).each do |f|
|
68
70
|
if f.index('.rb')
|
69
71
|
require(File.expand_path(File.join(File.dirname(__FILE__), f)))
|
70
72
|
next
|
data/lib/tml/application.rb
CHANGED
@@ -237,7 +237,7 @@ class Tml::Application < Tml::Base
|
|
237
237
|
source = Tml::Source.new(:source => source_key, :application => self)
|
238
238
|
params << {
|
239
239
|
:source => source_key,
|
240
|
-
:keys => keys.values.collect{|tkey| tkey.to_hash(:label, :description, :locale, :level)}
|
240
|
+
:keys => keys.values.collect{|tkey| tkey.to_hash(:label, :description, :locale, :level, :syntax)}
|
241
241
|
}
|
242
242
|
source.reset_cache
|
243
243
|
end
|
@@ -307,6 +307,8 @@ class Tml::Application < Tml::Base
|
|
307
307
|
|
308
308
|
# Cache translations within application object
|
309
309
|
def cache_translations(locale, key, new_translations)
|
310
|
+
return if new_translations.nil?
|
311
|
+
|
310
312
|
self.translations ||= {}
|
311
313
|
self.translations[locale] ||= {}
|
312
314
|
self.translations[locale][key] = new_translations.collect do |t|
|
data/lib/tml/config.rb
CHANGED
@@ -242,6 +242,7 @@ module Tml
|
|
242
242
|
:trade => '™', # TM
|
243
243
|
},
|
244
244
|
:decoration => {
|
245
|
+
:anchor => "<a href='{$href}'>{$0}</a>",
|
245
246
|
:strong => '<strong>{$0}</strong>',
|
246
247
|
:bold => '<strong>{$0}</strong>',
|
247
248
|
:b => '<strong>{$0}</strong>',
|
@@ -280,6 +281,7 @@ module Tml
|
|
280
281
|
:trade => '™',
|
281
282
|
},
|
282
283
|
:decoration => {
|
284
|
+
:anchor => '{$0}',
|
283
285
|
:strong => '{$0}',
|
284
286
|
:bold => '{$0}',
|
285
287
|
:b => '{$0}',
|
@@ -409,6 +411,32 @@ module Tml
|
|
409
411
|
# @default_level
|
410
412
|
#end
|
411
413
|
|
414
|
+
def xmessage_rule_key_mapping
|
415
|
+
@rule_key_mapping ||= {
|
416
|
+
number: {
|
417
|
+
one: 'singular',
|
418
|
+
few: 'few',
|
419
|
+
many: 'many',
|
420
|
+
other: 'plural'
|
421
|
+
},
|
422
|
+
gender: {
|
423
|
+
male: 'male',
|
424
|
+
female: 'female',
|
425
|
+
neutral: 'neutral',
|
426
|
+
other: 'other',
|
427
|
+
},
|
428
|
+
date: {
|
429
|
+
future: 'future',
|
430
|
+
present: 'present',
|
431
|
+
past: 'past'
|
432
|
+
}
|
433
|
+
}
|
434
|
+
end
|
435
|
+
|
436
|
+
def xmessage_decoration_tokens
|
437
|
+
@xmessage_tokens ||= @default_tokens[:html][:decoration].keys
|
438
|
+
end
|
439
|
+
|
412
440
|
def default_locale
|
413
441
|
@locale[:default] || 'en'
|
414
442
|
end
|
data/lib/tml/language.rb
CHANGED
@@ -149,14 +149,17 @@ class Tml::Language < Tml::Base
|
|
149
149
|
:description => params[:description],
|
150
150
|
:locale => hash_value(params[:options], :locale) || Tml.session.block_option(:locale) || Tml.config.default_locale,
|
151
151
|
:level => hash_value(params[:options], :level) || Tml.session.block_option(:level) || Tml.config.default_level,
|
152
|
+
:syntax => hash_value(params[:options], :syntax),
|
152
153
|
:translations => []
|
153
154
|
})
|
154
155
|
|
155
|
-
# pp "Translating #{params[:label]} from: #{translation_key.locale.inspect} to #{locale.inspect}"
|
156
|
+
# pp "Translating #{params[:label]} from: #{translation_key.locale.inspect} to #{locale.inspect} with syntax #{translation_key.syntax}"
|
156
157
|
# Tml.logger.info("Translating #{params[:label]} from: #{translation_key.locale.inspect} to #{locale.inspect}")
|
157
158
|
|
158
159
|
params[:tokens] ||= {}
|
159
|
-
params[:tokens]
|
160
|
+
if params[:tokens].is_a?(Hash)
|
161
|
+
params[:tokens][:viewing_user] ||= Tml.session.current_user
|
162
|
+
end
|
160
163
|
|
161
164
|
if Tml.config.disabled? or application.nil?
|
162
165
|
return translation_key.substitute_tokens(params[:label], params[:tokens], self, params[:options]).tml_translated
|
@@ -212,6 +215,9 @@ class Tml::Language < Tml::Base
|
|
212
215
|
end
|
213
216
|
|
214
217
|
translation_key.translate(self, params[:tokens], params[:options]).tml_translated
|
218
|
+
rescue Exception => ex
|
219
|
+
pp ex, ex.backtrace
|
220
|
+
return params[:label]
|
215
221
|
end
|
216
222
|
alias :tr :translate
|
217
223
|
|
data/lib/tml/tokenizers/data.rb
CHANGED
@@ -54,11 +54,11 @@ module Tml
|
|
54
54
|
attr_accessor :text, :context, :tokens, :opts
|
55
55
|
|
56
56
|
def self.supported_tokens
|
57
|
-
[Tml::Tokens::Data, Tml::Tokens::Method, Tml::Tokens::Transform]
|
57
|
+
[Tml::Tokens::Data, Tml::Tokens::Method, Tml::Tokens::Transform, Tml::Tokens::Map]
|
58
58
|
end
|
59
59
|
|
60
60
|
def self.required?(label)
|
61
|
-
label.index(
|
61
|
+
label.index('{')
|
62
62
|
end
|
63
63
|
|
64
64
|
def initialize(text, context={}, opts={})
|
@@ -84,10 +84,12 @@ module Tml
|
|
84
84
|
|
85
85
|
def substitute(language, options = {})
|
86
86
|
label = self.text
|
87
|
+
|
87
88
|
tokens.each do |token|
|
88
89
|
next unless token_allowed?(token)
|
89
90
|
label = token.substitute(label, context, language, options)
|
90
91
|
end
|
92
|
+
|
91
93
|
label
|
92
94
|
end
|
93
95
|
|
@@ -53,7 +53,7 @@ module Tml
|
|
53
53
|
module Tokenizers
|
54
54
|
class Decoration
|
55
55
|
|
56
|
-
attr_reader :tokens, :fragments, :context, :
|
56
|
+
attr_reader :tokens, :fragments, :context, :label, :opts
|
57
57
|
|
58
58
|
RESERVED_TOKEN = 'tml'
|
59
59
|
|
@@ -63,14 +63,14 @@ module Tml
|
|
63
63
|
RE_LONG_TOKEN_END = '\[\/[\w]*\]' # [/link]
|
64
64
|
RE_HTML_TOKEN_START = '<[^\>]*>' # <link>
|
65
65
|
RE_HTML_TOKEN_END = '<\/[^\>]*>' # </link>
|
66
|
-
RE_TEXT = '[^\[\]<>]+'
|
66
|
+
RE_TEXT = '[^\[\]<>]+' # '[\w\s!.:{}\(\)\|,?]*'
|
67
67
|
|
68
68
|
def self.required?(label)
|
69
69
|
label.index('[') or label.index('<')
|
70
70
|
end
|
71
71
|
|
72
|
-
def initialize(
|
73
|
-
@
|
72
|
+
def initialize(label, context = {}, opts = {})
|
73
|
+
@label = label
|
74
74
|
@context = context
|
75
75
|
@opts = opts
|
76
76
|
tokenize
|
@@ -84,12 +84,12 @@ module Tml
|
|
84
84
|
RE_HTML_TOKEN_START,
|
85
85
|
RE_HTML_TOKEN_END,
|
86
86
|
RE_TEXT].join('|')
|
87
|
-
@fragments =
|
87
|
+
@fragments = "[#{RESERVED_TOKEN}]#{@label}[/#{RESERVED_TOKEN}]".scan(/#{re}/)
|
88
88
|
@tokens = []
|
89
89
|
end
|
90
90
|
|
91
91
|
def parse
|
92
|
-
return @
|
92
|
+
return @label unless fragments
|
93
93
|
token = fragments.shift
|
94
94
|
|
95
95
|
if token.match(/#{RE_SHORT_TOKEN_START}/)
|
@@ -136,66 +136,9 @@ module Tml
|
|
136
136
|
tree
|
137
137
|
end
|
138
138
|
|
139
|
-
def
|
140
|
-
|
141
|
-
|
142
|
-
unless default_decoration
|
143
|
-
Tml.logger.error("Invalid decoration token value for #{token_name} in #{text}")
|
144
|
-
return token_value
|
145
|
-
end
|
146
|
-
|
147
|
-
default_decoration = default_decoration.clone
|
148
|
-
decoration_token_values = context[token_name.to_sym] || context[token_name.to_s]
|
149
|
-
|
150
|
-
default_decoration.gsub!('{$0}', token_value.to_s)
|
151
|
-
|
152
|
-
if decoration_token_values.is_a?(Hash)
|
153
|
-
decoration_token_values.keys.each do |key|
|
154
|
-
default_decoration.gsub!("{$#{key}}", decoration_token_values[key].to_s)
|
155
|
-
end
|
156
|
-
end
|
157
|
-
|
158
|
-
# remove unused attributes
|
159
|
-
default_decoration = default_decoration.gsub(/\{\$[^}]*\}/, '')
|
160
|
-
default_decoration
|
161
|
-
end
|
162
|
-
|
163
|
-
def allowed_token?(token)
|
164
|
-
return true if opts[:allowed_tokens].nil?
|
165
|
-
opts[:allowed_tokens].include?(token)
|
166
|
-
end
|
167
|
-
|
168
|
-
def apply(token, value)
|
169
|
-
return value if token == RESERVED_TOKEN
|
170
|
-
return value unless allowed_token?(token)
|
171
|
-
|
172
|
-
method = context[token.to_sym] || context[token.to_s]
|
173
|
-
|
174
|
-
if method
|
175
|
-
if method.is_a?(Proc)
|
176
|
-
return method.call(value)
|
177
|
-
end
|
178
|
-
|
179
|
-
if method.is_a?(Array) or method.is_a?(Hash)
|
180
|
-
return default_decoration(token, value)
|
181
|
-
end
|
182
|
-
|
183
|
-
if method.is_a?(String)
|
184
|
-
return method.to_s.gsub('{$0}', value)
|
185
|
-
end
|
186
|
-
|
187
|
-
return value
|
188
|
-
end
|
189
|
-
|
190
|
-
if Tml.config.default_token_value(normalize_token(token), :decoration)
|
191
|
-
return default_decoration(token, value)
|
192
|
-
end
|
193
|
-
|
194
|
-
value
|
195
|
-
end
|
196
|
-
|
197
|
-
def normalize_token(name)
|
198
|
-
name.to_s.gsub(/(\d)*$/, '')
|
139
|
+
def apply(token_name, value)
|
140
|
+
token = ::Tml::Tokens::Decoration.new(label, token_name)
|
141
|
+
token.apply(context, value, opts[:allowed_tokens])
|
199
142
|
end
|
200
143
|
|
201
144
|
def evaluate(expr)
|
@@ -211,7 +154,7 @@ module Tml
|
|
211
154
|
end
|
212
155
|
|
213
156
|
def substitute
|
214
|
-
evaluate(parse).gsub(
|
157
|
+
evaluate(parse).gsub(/[<\[]\/tml[>\]]/, '')
|
215
158
|
end
|
216
159
|
|
217
160
|
end
|
data/lib/tml/tokenizers/dom.rb
CHANGED
@@ -0,0 +1,490 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
#--
|
3
|
+
# Copyright (c) 2016 Translation Exchange, Inc
|
4
|
+
#
|
5
|
+
# _______ _ _ _ ______ _
|
6
|
+
# |__ __| | | | | (_) | ____| | |
|
7
|
+
# | |_ __ __ _ _ __ ___| | __ _| |_ _ ___ _ __ | |__ __ _____| |__ __ _ _ __ __ _ ___
|
8
|
+
# | | '__/ _` | '_ \/ __| |/ _` | __| |/ _ \| '_ \| __| \ \/ / __| '_ \ / _` | '_ \ / _` |/ _ \
|
9
|
+
# | | | | (_| | | | \__ \ | (_| | |_| | (_) | | | | |____ > < (__| | | | (_| | | | | (_| | __/
|
10
|
+
# |_|_| \__,_|_| |_|___/_|\__,_|\__|_|\___/|_| |_|______/_/\_\___|_| |_|\__,_|_| |_|\__, |\___|
|
11
|
+
# __/ |
|
12
|
+
# |___/
|
13
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
14
|
+
# a copy of this software and associated documentation files (the
|
15
|
+
# "Software"), to deal in the Software without restriction, including
|
16
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
17
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
18
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
19
|
+
# the following conditions:
|
20
|
+
#
|
21
|
+
# The above copyright notice and this permission notice shall be
|
22
|
+
# included in all copies or substantial portions of the Software.
|
23
|
+
#
|
24
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
25
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
26
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
27
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
28
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
29
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
30
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
31
|
+
#++
|
32
|
+
|
33
|
+
#######################################################################
|
34
|
+
#
|
35
|
+
# Decoration Token Forms:
|
36
|
+
#
|
37
|
+
# [link: click here]
|
38
|
+
# or
|
39
|
+
# [link] click here [/link]
|
40
|
+
#
|
41
|
+
# Decoration Tokens Allow Nesting:
|
42
|
+
#
|
43
|
+
# [link: {count} {_messages}]
|
44
|
+
# [link: {count||message}]
|
45
|
+
# [link: {count||person, people}]
|
46
|
+
# [link: {user.name}]
|
47
|
+
#
|
48
|
+
#######################################################################
|
49
|
+
|
50
|
+
module Tml
|
51
|
+
module Tokenizers
|
52
|
+
class XMessage
|
53
|
+
|
54
|
+
attr_accessor :label, :pos, :len, :last, :options, :tree
|
55
|
+
|
56
|
+
def optional_style_format_types
|
57
|
+
@optional_style_format_types ||= {
|
58
|
+
'text' => true,
|
59
|
+
'date' => true,
|
60
|
+
'time' => true,
|
61
|
+
'number' => true,
|
62
|
+
'name' => true,
|
63
|
+
'list' => true,
|
64
|
+
'possessive' => true,
|
65
|
+
'salutation' => true
|
66
|
+
}
|
67
|
+
end
|
68
|
+
|
69
|
+
def initialize(text, opts = {})
|
70
|
+
@label = text
|
71
|
+
@pos = 0
|
72
|
+
@len = @label ? @label.length : 0
|
73
|
+
@last = nil
|
74
|
+
@options = opts || {}
|
75
|
+
@tree = nil
|
76
|
+
tokenize
|
77
|
+
end
|
78
|
+
|
79
|
+
def update_last
|
80
|
+
@last = @pos > 0 ? @label[@pos - 1] : nil
|
81
|
+
end
|
82
|
+
|
83
|
+
def next_char
|
84
|
+
return if @len == 0 || @pos >= @len
|
85
|
+
update_last
|
86
|
+
@pos += 1
|
87
|
+
@label[@pos - 1]
|
88
|
+
end
|
89
|
+
|
90
|
+
def peek_char
|
91
|
+
return if @len == 0
|
92
|
+
@label[@pos]
|
93
|
+
end
|
94
|
+
|
95
|
+
def revert
|
96
|
+
if (@pos > 0)
|
97
|
+
@pos -= 1
|
98
|
+
update_last
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def escaped?
|
103
|
+
@last && @last == '\\'
|
104
|
+
end
|
105
|
+
|
106
|
+
def no_format_style(result, c, argument_index, format_type)
|
107
|
+
raise "no format style allowed for format type '" + format_type + "'";
|
108
|
+
end
|
109
|
+
|
110
|
+
def collection_format_style(result, c, argument_index, format_type)
|
111
|
+
# register the format element
|
112
|
+
styles = []
|
113
|
+
subtype = 'text'; # default
|
114
|
+
|
115
|
+
if c == ','
|
116
|
+
# we have a sub-type
|
117
|
+
subtype = ''
|
118
|
+
c = next_char
|
119
|
+
while c && !',}'.index(c)
|
120
|
+
subtype += c
|
121
|
+
c = next_char
|
122
|
+
unless c
|
123
|
+
raise "expected ',' or '}', but found end of string"
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
result << {index: argument_index, type: format_type, subtype: subtype, styles: styles}
|
129
|
+
|
130
|
+
if c == '}'
|
131
|
+
return
|
132
|
+
end
|
133
|
+
|
134
|
+
# parse format style
|
135
|
+
while c
|
136
|
+
c = next_char
|
137
|
+
unless c
|
138
|
+
raise "expected '}', '|' or format style value, but found end of string"
|
139
|
+
end
|
140
|
+
|
141
|
+
if c == '}' && !escaped?
|
142
|
+
return
|
143
|
+
elsif c == '|'
|
144
|
+
next
|
145
|
+
end
|
146
|
+
|
147
|
+
style_key = ''
|
148
|
+
while c && !'#<|}'.index(c)
|
149
|
+
style_key += c
|
150
|
+
c = next_char
|
151
|
+
unless c
|
152
|
+
raise "expected '#', '<' or '|', but found end of string"
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
if c == '<'
|
157
|
+
style_key += c
|
158
|
+
end
|
159
|
+
|
160
|
+
items = []
|
161
|
+
styles << {key: style_key, items: items}
|
162
|
+
|
163
|
+
if '#<'.index(c)
|
164
|
+
traverse_text(items)
|
165
|
+
elsif '|}'.index(c)
|
166
|
+
# we found a key without value e.g. {0,param,possessive} and {0,param,prefix#.|possessive}
|
167
|
+
revert
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
def text_format_style(result, c, argument_index, format_type)
|
173
|
+
# parse format style
|
174
|
+
buffer = ''
|
175
|
+
c = next_char
|
176
|
+
unless c
|
177
|
+
raise "expected format style or '}', but found end of string"
|
178
|
+
end
|
179
|
+
|
180
|
+
while c
|
181
|
+
if c == '}'
|
182
|
+
result << {index: argument_index, type: format_type, value: buffer}
|
183
|
+
return
|
184
|
+
end
|
185
|
+
|
186
|
+
# keep adding to buffer
|
187
|
+
buffer += c
|
188
|
+
c = next_char
|
189
|
+
unless c
|
190
|
+
raise "expected '}', but found end of string"
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
def default_format_style(result, c, argument_index, format_type)
|
196
|
+
# register the format element
|
197
|
+
styles = []
|
198
|
+
result << {index: argument_index, type: format_type, styles: styles}
|
199
|
+
|
200
|
+
# parse format style
|
201
|
+
while c
|
202
|
+
c = next_char
|
203
|
+
unless c
|
204
|
+
raise "expected '}', '|' or format style value, but found end of string"
|
205
|
+
end
|
206
|
+
|
207
|
+
if c == '}' && !escaped?
|
208
|
+
return
|
209
|
+
elsif c == '|'
|
210
|
+
next
|
211
|
+
end
|
212
|
+
|
213
|
+
style_key = ''
|
214
|
+
while c && !'#<+|}'.index(c)
|
215
|
+
style_key += c
|
216
|
+
c = next_char
|
217
|
+
unless c
|
218
|
+
raise "expected '#', '<', '+' or '|', but found end of string"
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
if c == '<' || c == '+'
|
223
|
+
style_key += c
|
224
|
+
end
|
225
|
+
|
226
|
+
items = []
|
227
|
+
styles << {key: style_key, items: items}
|
228
|
+
|
229
|
+
if '#<+'.index(c)
|
230
|
+
traverse_text(items)
|
231
|
+
elsif '|}'.index(c)
|
232
|
+
# we found a key without value e.g. {0,param,possessive} and {0,param,prefix#.|possessive}
|
233
|
+
revert
|
234
|
+
end
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
def traverse_format_element(result)
|
239
|
+
argument_index = -1
|
240
|
+
format_type = nil
|
241
|
+
c = next_char
|
242
|
+
|
243
|
+
unless c
|
244
|
+
raise 'expected place holder index, but found end of string'
|
245
|
+
end
|
246
|
+
|
247
|
+
if c.match(/[\d:]/)
|
248
|
+
# process argument index
|
249
|
+
is_keyword = c == ':'
|
250
|
+
index = ''
|
251
|
+
while c && !',}'.index(c)
|
252
|
+
index += c
|
253
|
+
c = next_char
|
254
|
+
unless c
|
255
|
+
raise "expected ',' or '}', but found end of string";
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
if !is_keyword && !index.match(/\d+/)
|
260
|
+
throw "argument index must be numeric: #{index}"
|
261
|
+
end
|
262
|
+
|
263
|
+
argument_index = is_keyword ? index : index * 1
|
264
|
+
end
|
265
|
+
|
266
|
+
if c != '}'
|
267
|
+
# process format type
|
268
|
+
format_type = ''
|
269
|
+
c = next_char
|
270
|
+
unless c
|
271
|
+
raise 'expected format type, but found end of string'
|
272
|
+
end
|
273
|
+
|
274
|
+
while c && !',}'.index(c) && !escaped?
|
275
|
+
format_type += c
|
276
|
+
c = next_char
|
277
|
+
unless c
|
278
|
+
raise "expected ',' or '}', but found end of string"
|
279
|
+
end
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
if c == '}' && !escaped?
|
284
|
+
if format_type && optional_style_format_types[format_type]
|
285
|
+
# we found {0,number} or {0,possessive} or {0,salutation}, which are valid expressions
|
286
|
+
result << {type: format_type, index: argument_index}
|
287
|
+
else
|
288
|
+
if format_type
|
289
|
+
# we found something like {0,<type>}, which is invalid.
|
290
|
+
raise "expected format style for format type '#{format_type}'"
|
291
|
+
end
|
292
|
+
|
293
|
+
# push param format element
|
294
|
+
result << {type: 'param', index: argument_index}
|
295
|
+
end
|
296
|
+
elsif c == ','
|
297
|
+
processors = {
|
298
|
+
list: 'collection_format_style',
|
299
|
+
date: 'text_format_style',
|
300
|
+
time: 'text_format_style',
|
301
|
+
number: 'text_format_style',
|
302
|
+
suffix: 'text_format_style',
|
303
|
+
possessive: 'no_format_style',
|
304
|
+
salutation: 'no_format_style',
|
305
|
+
default: 'default_format_style'
|
306
|
+
}
|
307
|
+
processor = (processors[format_type.to_sym] || processors[:default])
|
308
|
+
self.send(processor, result, c, argument_index, format_type)
|
309
|
+
else
|
310
|
+
raise "expected ',' or '}', but found '#{c}' at position #{@pos}"
|
311
|
+
end
|
312
|
+
end
|
313
|
+
|
314
|
+
def traverse_text(result)
|
315
|
+
in_quoted_string = false
|
316
|
+
buffer = ''
|
317
|
+
c = next_char
|
318
|
+
|
319
|
+
while c do
|
320
|
+
if c == "'"
|
321
|
+
in_quoted_string = !in_quoted_string
|
322
|
+
end
|
323
|
+
|
324
|
+
if !in_quoted_string && c == '{' && !escaped?
|
325
|
+
unless buffer.empty?
|
326
|
+
result << {type: 'trans', value: buffer}
|
327
|
+
buffer = ''
|
328
|
+
end
|
329
|
+
traverse_format_element(result)
|
330
|
+
elsif !in_quoted_string && (c == '|' || c == '}') && !escaped?
|
331
|
+
revert
|
332
|
+
break
|
333
|
+
else
|
334
|
+
buffer += c
|
335
|
+
end
|
336
|
+
c = next_char
|
337
|
+
end
|
338
|
+
|
339
|
+
unless buffer.empty?
|
340
|
+
result << {type: 'trans', value: buffer}
|
341
|
+
buffer = ''
|
342
|
+
end
|
343
|
+
|
344
|
+
result
|
345
|
+
end
|
346
|
+
|
347
|
+
def tokenize
|
348
|
+
result = []
|
349
|
+
traverse_text(result)
|
350
|
+
@tree = result
|
351
|
+
rescue Exception => ex
|
352
|
+
pp ex
|
353
|
+
pp "Failed to parse the expression: " + @label
|
354
|
+
@tree = nil
|
355
|
+
end
|
356
|
+
|
357
|
+
def rule_key(context_key, rule_key)
|
358
|
+
return rule_key unless Tml.config.xmessage_rule_key_mapping[context_key.to_sym]
|
359
|
+
Tml.config.xmessage_rule_key_mapping[context_key.to_sym][rule_key.to_sym] || rule_key
|
360
|
+
end
|
361
|
+
|
362
|
+
def choice(language, token, token_object)
|
363
|
+
return unless token
|
364
|
+
|
365
|
+
context_key = token.context_keys.first
|
366
|
+
return unless context_key
|
367
|
+
|
368
|
+
ctx = language.context_by_keyword(context_key)
|
369
|
+
return unless ctx
|
370
|
+
|
371
|
+
# pp context_key, token_object
|
372
|
+
|
373
|
+
rule = ctx.find_matching_rule(token_object)
|
374
|
+
if rule
|
375
|
+
# pp context_key, rule.keyword
|
376
|
+
return rule_key(context_key, rule.keyword)
|
377
|
+
end
|
378
|
+
|
379
|
+
nil
|
380
|
+
end
|
381
|
+
|
382
|
+
def data?(type)
|
383
|
+
return false unless type
|
384
|
+
%w(param number).include?(type)
|
385
|
+
end
|
386
|
+
|
387
|
+
def decoration?(type)
|
388
|
+
return false unless type
|
389
|
+
Tml.config.xmessage_decoration_tokens.include?(type.to_sym)
|
390
|
+
end
|
391
|
+
|
392
|
+
def choice?(type)
|
393
|
+
type == 'choice'
|
394
|
+
end
|
395
|
+
|
396
|
+
def map?(type)
|
397
|
+
type == 'map'
|
398
|
+
end
|
399
|
+
|
400
|
+
def compile(language, exp, buffer, params)
|
401
|
+
style = nil
|
402
|
+
|
403
|
+
exp.each do |el|
|
404
|
+
token = token_by_type(el[:type], el)
|
405
|
+
token_object = get_token_object(params, token)
|
406
|
+
token_value = get_token_value(token_object, token, language)
|
407
|
+
|
408
|
+
if el[:styles]
|
409
|
+
if choice?(el[:type])
|
410
|
+
key = choice(language, token, token_object)
|
411
|
+
style = el[:styles].find{ |style|
|
412
|
+
style[:key] == key
|
413
|
+
}
|
414
|
+
if style
|
415
|
+
compile(language, style[:items], buffer, params)
|
416
|
+
end
|
417
|
+
elsif map?(el[:type])
|
418
|
+
style = el[:styles].find{ |style|
|
419
|
+
style[:key] == token_value
|
420
|
+
}
|
421
|
+
compile(language, style[:items], buffer, params)
|
422
|
+
elsif decoration?(el[:type])
|
423
|
+
buffer << token.open_tag(token_object)
|
424
|
+
compile(language, el[:styles][0][:items], buffer, params)
|
425
|
+
buffer << token.close_tag
|
426
|
+
else
|
427
|
+
compile(language, el[:styles][0][:items], buffer, params)
|
428
|
+
end
|
429
|
+
elsif data?(el[:type])
|
430
|
+
buffer << token_value
|
431
|
+
else
|
432
|
+
buffer << el[:value]
|
433
|
+
end
|
434
|
+
end
|
435
|
+
|
436
|
+
buffer
|
437
|
+
end
|
438
|
+
|
439
|
+
def tokens
|
440
|
+
@tokens ||= begin
|
441
|
+
tokens = []
|
442
|
+
extract_tokens(tree, tokens)
|
443
|
+
tokens.uniq{ |t| [t.class.name, t.full_name] }
|
444
|
+
end
|
445
|
+
end
|
446
|
+
|
447
|
+
def token_by_type(type, data)
|
448
|
+
if decoration?(type)
|
449
|
+
Tml::Tokens::XMessage::Decoration.new(label, data)
|
450
|
+
elsif data?(type)
|
451
|
+
Tml::Tokens::XMessage::Data.new(label, data)
|
452
|
+
elsif choice?(type)
|
453
|
+
Tml::Tokens::XMessage::Choice.new(label, data)
|
454
|
+
elsif map?(type)
|
455
|
+
return Tml::Tokens::XMessage::Map.new(label, data)
|
456
|
+
else
|
457
|
+
nil
|
458
|
+
end
|
459
|
+
end
|
460
|
+
|
461
|
+
def extract_tokens(tree, tokens)
|
462
|
+
tree.each do |fragment|
|
463
|
+
token = token_by_type(fragment[:type], fragment)
|
464
|
+
tokens << token if token
|
465
|
+
if fragment[:items]
|
466
|
+
extract_tokens(fragment[:items], tokens)
|
467
|
+
elsif fragment[:styles]
|
468
|
+
extract_tokens(fragment[:styles], tokens)
|
469
|
+
end
|
470
|
+
end
|
471
|
+
end
|
472
|
+
|
473
|
+
def get_token_object(token_values, token)
|
474
|
+
return nil unless token
|
475
|
+
token.token_object(token_values)
|
476
|
+
end
|
477
|
+
|
478
|
+
def get_token_value(token_object, token, language)
|
479
|
+
return nil unless token_object && token
|
480
|
+
token.token_value(token_object, language)
|
481
|
+
end
|
482
|
+
|
483
|
+
def substitute(language, tokens = {}, options = {})
|
484
|
+
return @label unless tree
|
485
|
+
compile(language, tree, [], tokens).join('')
|
486
|
+
end
|
487
|
+
|
488
|
+
end
|
489
|
+
end
|
490
|
+
end
|