tml 5.6.5 → 5.7.1
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.
- 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
|