twitter_cldr_js 1.0.0 → 2.0.0
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.
- data/Gemfile +5 -0
- data/History.txt +11 -1
- data/README.md +103 -13
- data/Rakefile +1 -1
- data/lib/assets/javascripts/twitter_cldr/af.js +1815 -0
- data/lib/assets/javascripts/twitter_cldr/ar.js +1815 -0
- data/lib/assets/javascripts/twitter_cldr/be.js +1815 -0
- data/lib/assets/javascripts/twitter_cldr/bg.js +1815 -0
- data/lib/assets/javascripts/twitter_cldr/bn.js +1815 -0
- data/lib/assets/javascripts/twitter_cldr/ca.js +1815 -0
- data/lib/assets/javascripts/twitter_cldr/cs.js +1815 -0
- data/lib/assets/javascripts/twitter_cldr/cy.js +1815 -0
- data/lib/assets/javascripts/twitter_cldr/da.js +1815 -0
- data/lib/assets/javascripts/twitter_cldr/de.js +1815 -0
- data/lib/assets/javascripts/twitter_cldr/el.js +1815 -0
- data/lib/assets/javascripts/twitter_cldr/en.js +1815 -0
- data/lib/assets/javascripts/twitter_cldr/es.js +1815 -0
- data/lib/assets/javascripts/twitter_cldr/eu.js +1815 -0
- data/lib/assets/javascripts/twitter_cldr/fa.js +1815 -0
- data/lib/assets/javascripts/twitter_cldr/fi.js +1815 -0
- data/lib/assets/javascripts/twitter_cldr/fil.js +1815 -0
- data/lib/assets/javascripts/twitter_cldr/fr.js +1815 -0
- data/lib/assets/javascripts/twitter_cldr/ga.js +1815 -0
- data/lib/assets/javascripts/twitter_cldr/gl.js +1815 -0
- data/lib/assets/javascripts/twitter_cldr/he.js +1815 -0
- data/lib/assets/javascripts/twitter_cldr/hi.js +1815 -0
- data/lib/assets/javascripts/twitter_cldr/hu.js +1815 -0
- data/lib/assets/javascripts/twitter_cldr/id.js +1815 -0
- data/lib/assets/javascripts/twitter_cldr/it.js +1815 -0
- data/lib/assets/javascripts/twitter_cldr/ja.js +1815 -0
- data/lib/assets/javascripts/twitter_cldr/ko.js +1815 -0
- data/lib/assets/javascripts/twitter_cldr/lv.js +1815 -0
- data/lib/assets/javascripts/twitter_cldr/msa.js +1815 -0
- data/lib/assets/javascripts/twitter_cldr/nl.js +1815 -0
- data/lib/assets/javascripts/twitter_cldr/no.js +1815 -0
- data/lib/assets/javascripts/twitter_cldr/pl.js +1815 -0
- data/lib/assets/javascripts/twitter_cldr/pt.js +1815 -0
- data/lib/assets/javascripts/twitter_cldr/ro.js +1815 -0
- data/lib/assets/javascripts/twitter_cldr/ru.js +1815 -0
- data/lib/assets/javascripts/twitter_cldr/sk.js +1815 -0
- data/lib/assets/javascripts/twitter_cldr/sq.js +1815 -0
- data/lib/assets/javascripts/twitter_cldr/sr.js +1815 -0
- data/lib/assets/javascripts/twitter_cldr/sv.js +1815 -0
- data/lib/assets/javascripts/twitter_cldr/ta.js +1815 -0
- data/lib/assets/javascripts/twitter_cldr/th.js +1815 -0
- data/lib/assets/javascripts/twitter_cldr/tr.js +1815 -0
- data/lib/assets/javascripts/twitter_cldr/uk.js +1815 -0
- data/lib/assets/javascripts/twitter_cldr/ur.js +1815 -0
- data/lib/assets/javascripts/twitter_cldr/zh-cn.js +1815 -0
- data/lib/assets/javascripts/twitter_cldr/zh-tw.js +1815 -0
- data/lib/twitter_cldr/js/compiler.rb +12 -3
- data/lib/twitter_cldr/js/mustache/bundle.coffee +19 -1
- data/lib/twitter_cldr/js/mustache/calendars/additional_date_format_selector.coffee +85 -0
- data/lib/twitter_cldr/js/mustache/calendars/datetime.coffee +248 -226
- data/lib/twitter_cldr/js/mustache/calendars/timespan.coffee +19 -18
- data/lib/twitter_cldr/js/mustache/numbers/numbers.coffee +62 -19
- data/lib/twitter_cldr/js/mustache/plurals/rules.coffee +1 -1
- data/lib/twitter_cldr/js/mustache/shared/bidi.coffee +433 -0
- data/lib/twitter_cldr/js/mustache/shared/calendar.coffee +25 -0
- data/lib/twitter_cldr/js/mustache/shared/currencies.coffee +5 -11
- data/lib/twitter_cldr/js/mustache/shared/lists.coffee +40 -0
- data/lib/twitter_cldr/js/mustache/shared/prerender/bidi_classes.json +1 -0
- data/lib/twitter_cldr/js/mustache/utilities.coffee +87 -0
- data/lib/twitter_cldr/js/renderers.rb +23 -19
- data/lib/twitter_cldr/js/renderers/bundle.rb +8 -2
- data/lib/twitter_cldr/js/renderers/calendars/additional_date_format_selector_renderer.rb +18 -0
- data/lib/twitter_cldr/js/renderers/calendars/datetime_renderer.rb +13 -9
- data/lib/twitter_cldr/js/renderers/numbers/numbers_renderer.rb +17 -3
- data/lib/twitter_cldr/js/renderers/shared/bidi_renderer.rb +77 -0
- data/lib/twitter_cldr/js/renderers/shared/calendar_renderer.rb +20 -0
- data/lib/twitter_cldr/js/renderers/shared/currencies_renderer.rb +2 -2
- data/lib/twitter_cldr/js/renderers/shared/list_renderer.rb +22 -0
- data/lib/twitter_cldr/js/tasks/tasks.rake +9 -5
- data/lib/twitter_cldr/js/tasks/tasks.rb +6 -5
- data/lib/twitter_cldr/js/version.rb +1 -1
- data/spec/js/calendars/additional_date_format_selector.spec.js +126 -0
- data/spec/js/calendars/datetime.spec.js +29 -2
- data/spec/js/calendars/timespan.spec.js +45 -1
- data/spec/js/numbers/abbreviated/abbreviated_number.spec.js +47 -0
- data/spec/js/numbers/abbreviated/long_decimal.spec.js +57 -0
- data/spec/js/numbers/abbreviated/short_decimal.spec.js +57 -0
- data/spec/js/numbers/currency.spec.js +2 -2
- data/spec/js/numbers/decimal.spec.js +1 -1
- data/spec/js/numbers/helpers/fraction.spec.js +1 -1
- data/spec/js/numbers/helpers/integer.spec.js +1 -1
- data/spec/js/numbers/number.spec.js +1 -1
- data/spec/js/numbers/percent.spec.js +1 -1
- data/spec/js/plurals/plural_rules.spec.js +1 -1
- data/spec/js/shared/bidi.spec.js +105 -0
- data/spec/js/shared/calendar.spec.js +51 -0
- data/spec/js/shared/classpath_bidi_test.txt +217202 -0
- data/spec/js/shared/lists.spec.js +100 -0
- data/spec/js/utilities.spec.js +120 -0
- data/twitter_cldr_js.gemspec +1 -1
- metadata +68 -37
- data/lib/assets/javascripts/twitter_cldr/twitter_cldr_af.js +0 -887
- data/lib/assets/javascripts/twitter_cldr/twitter_cldr_ar.js +0 -887
- data/lib/assets/javascripts/twitter_cldr/twitter_cldr_ca.js +0 -887
- data/lib/assets/javascripts/twitter_cldr/twitter_cldr_cs.js +0 -887
- data/lib/assets/javascripts/twitter_cldr/twitter_cldr_da.js +0 -887
- data/lib/assets/javascripts/twitter_cldr/twitter_cldr_de.js +0 -887
- data/lib/assets/javascripts/twitter_cldr/twitter_cldr_el.js +0 -887
- data/lib/assets/javascripts/twitter_cldr/twitter_cldr_en.js +0 -887
- data/lib/assets/javascripts/twitter_cldr/twitter_cldr_es.js +0 -887
- data/lib/assets/javascripts/twitter_cldr/twitter_cldr_eu.js +0 -887
- data/lib/assets/javascripts/twitter_cldr/twitter_cldr_fa.js +0 -887
- data/lib/assets/javascripts/twitter_cldr/twitter_cldr_fi.js +0 -887
- data/lib/assets/javascripts/twitter_cldr/twitter_cldr_fil.js +0 -887
- data/lib/assets/javascripts/twitter_cldr/twitter_cldr_fr.js +0 -887
- data/lib/assets/javascripts/twitter_cldr/twitter_cldr_he.js +0 -887
- data/lib/assets/javascripts/twitter_cldr/twitter_cldr_hi.js +0 -887
- data/lib/assets/javascripts/twitter_cldr/twitter_cldr_hu.js +0 -887
- data/lib/assets/javascripts/twitter_cldr/twitter_cldr_id.js +0 -887
- data/lib/assets/javascripts/twitter_cldr/twitter_cldr_it.js +0 -887
- data/lib/assets/javascripts/twitter_cldr/twitter_cldr_ja.js +0 -887
- data/lib/assets/javascripts/twitter_cldr/twitter_cldr_ko.js +0 -887
- data/lib/assets/javascripts/twitter_cldr/twitter_cldr_msa.js +0 -887
- data/lib/assets/javascripts/twitter_cldr/twitter_cldr_nl.js +0 -887
- data/lib/assets/javascripts/twitter_cldr/twitter_cldr_no.js +0 -887
- data/lib/assets/javascripts/twitter_cldr/twitter_cldr_pl.js +0 -887
- data/lib/assets/javascripts/twitter_cldr/twitter_cldr_pt.js +0 -887
- data/lib/assets/javascripts/twitter_cldr/twitter_cldr_ru.js +0 -887
- data/lib/assets/javascripts/twitter_cldr/twitter_cldr_sv.js +0 -887
- data/lib/assets/javascripts/twitter_cldr/twitter_cldr_th.js +0 -887
- data/lib/assets/javascripts/twitter_cldr/twitter_cldr_tr.js +0 -887
- data/lib/assets/javascripts/twitter_cldr/twitter_cldr_uk.js +0 -887
- data/lib/assets/javascripts/twitter_cldr/twitter_cldr_ur.js +0 -887
- data/lib/assets/javascripts/twitter_cldr/twitter_cldr_zh-cn.js +0 -887
- data/lib/assets/javascripts/twitter_cldr/twitter_cldr_zh-tw.js +0 -887
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
# Copyright 2012 Twitter, Inc
|
|
2
2
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
3
3
|
|
|
4
|
-
TwitterCldr.TimespanFormatter
|
|
4
|
+
class TwitterCldr.TimespanFormatter
|
|
5
5
|
constructor: ->
|
|
6
|
+
@approximate_multiplier = 0.75
|
|
6
7
|
@default_type = "default"
|
|
7
8
|
@tokens = `{{{tokens}}}`
|
|
8
9
|
@time_in_seconds = {
|
|
@@ -15,9 +16,11 @@ TwitterCldr.TimespanFormatter = class TimespanFormatter
|
|
|
15
16
|
"year": 31556926
|
|
16
17
|
}
|
|
17
18
|
|
|
18
|
-
format: (seconds,
|
|
19
|
+
format: (seconds, fmt_options = {}) ->
|
|
20
|
+
options = {}
|
|
21
|
+
options[key] = obj for key, obj of fmt_options
|
|
19
22
|
options["direction"] ||= (if seconds < 0 then "ago" else "until")
|
|
20
|
-
options["unit"] = this.calculate_unit(Math.abs(seconds)) if options["unit"] is null or options["unit"] is undefined
|
|
23
|
+
options["unit"] = this.calculate_unit(Math.abs(seconds), options) if options["unit"] is null or options["unit"] is undefined
|
|
21
24
|
options["type"] ||= @default_type
|
|
22
25
|
options["number"] = this.calculate_time(Math.abs(seconds), options["unit"])
|
|
23
26
|
number = this.calculate_time(Math.abs(seconds), options["unit"])
|
|
@@ -26,21 +29,19 @@ TwitterCldr.TimespanFormatter = class TimespanFormatter
|
|
|
26
29
|
strings = (token.value for token in @tokens[options["direction"]][options["unit"]][options["type"]][options["rule"]])
|
|
27
30
|
strings.join("").replace(/\{[0-9]\}/, number.toString())
|
|
28
31
|
|
|
29
|
-
calculate_unit: (seconds) ->
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
else if seconds <
|
|
37
|
-
|
|
38
|
-
else if seconds <
|
|
39
|
-
|
|
40
|
-
else if seconds <
|
|
41
|
-
|
|
42
|
-
else
|
|
43
|
-
"year"
|
|
32
|
+
calculate_unit: (seconds, unit_options = {}) ->
|
|
33
|
+
options = {}
|
|
34
|
+
options[key] = obj for key, obj of unit_options
|
|
35
|
+
options["approximate"] = false unless options.approximate?
|
|
36
|
+
multiplier = if options.approximate then @approximate_multiplier else 1
|
|
37
|
+
|
|
38
|
+
if seconds < (@time_in_seconds.minute * multiplier) then "second"
|
|
39
|
+
else if seconds < (@time_in_seconds.hour * multiplier) then "minute"
|
|
40
|
+
else if seconds < (@time_in_seconds.day * multiplier) then "hour"
|
|
41
|
+
else if seconds < (@time_in_seconds.week * multiplier) then "day"
|
|
42
|
+
else if seconds < (@time_in_seconds.month * multiplier) then "week"
|
|
43
|
+
else if seconds < (@time_in_seconds.year * multiplier) then "month"
|
|
44
|
+
else "year"
|
|
44
45
|
|
|
45
46
|
# 0 <-> 29 secs # => seconds
|
|
46
47
|
# 30 secs <-> 44 mins, 29 secs # => minutes
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
# Copyright 2012 Twitter, Inc
|
|
2
2
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
3
3
|
|
|
4
|
-
TwitterCldr.NumberFormatter
|
|
5
|
-
|
|
4
|
+
class TwitterCldr.NumberFormatter
|
|
5
|
+
constructor: ->
|
|
6
6
|
@all_tokens = `{{{tokens}}}`
|
|
7
7
|
@tokens = []
|
|
8
8
|
@symbols = `{{{symbols}}}`
|
|
9
9
|
|
|
10
|
-
@default_symbols =
|
|
10
|
+
@default_symbols =
|
|
11
11
|
'group': ','
|
|
12
12
|
'decimal': '.'
|
|
13
13
|
'plus_sign': '+'
|
|
@@ -20,17 +20,23 @@ TwitterCldr.NumberFormatter = class NumberFormatter
|
|
|
20
20
|
opts[key] = if options[key]? then options[key] else opts[key]
|
|
21
21
|
|
|
22
22
|
[prefix, suffix, integer_format, fraction_format] = this.partition_tokens(this.get_tokens(number, opts))
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
number = this.transform_number(number)
|
|
24
|
+
[intg, fraction] = this.parse_number(number, opts)
|
|
25
|
+
result = integer_format.apply(parseFloat(intg), opts)
|
|
25
26
|
result += fraction_format.apply(fraction, opts) if fraction
|
|
26
27
|
sign = if number < 0 && prefix != "-" then @symbols.minus_sign || @default_symbols.minus_sign else ""
|
|
27
28
|
"#{prefix}#{result}#{suffix}"
|
|
28
29
|
|
|
30
|
+
transform_number: (number) ->
|
|
31
|
+
number # noop for base class
|
|
32
|
+
|
|
29
33
|
partition_tokens: (tokens) ->
|
|
30
|
-
[
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
+
[
|
|
35
|
+
tokens[0] || "",
|
|
36
|
+
tokens[2] || "",
|
|
37
|
+
new TwitterCldr.NumberFormatter.IntegerHelper(tokens[1], @symbols),
|
|
38
|
+
new TwitterCldr.NumberFormatter.FractionHelper(tokens[1], @symbols)
|
|
39
|
+
]
|
|
34
40
|
|
|
35
41
|
parse_number: (number, options = {}) ->
|
|
36
42
|
if options.precision?
|
|
@@ -52,7 +58,7 @@ TwitterCldr.NumberFormatter = class NumberFormatter
|
|
|
52
58
|
get_tokens: ->
|
|
53
59
|
throw "get_tokens() not implemented - use a derived class like PercentFormatter."
|
|
54
60
|
|
|
55
|
-
TwitterCldr.PercentFormatter
|
|
61
|
+
class TwitterCldr.PercentFormatter extends TwitterCldr.NumberFormatter
|
|
56
62
|
constructor: (options = {}) ->
|
|
57
63
|
@default_percent_sign = "%"
|
|
58
64
|
super
|
|
@@ -66,7 +72,7 @@ TwitterCldr.PercentFormatter = class PercentFormatter extends NumberFormatter
|
|
|
66
72
|
get_tokens: (number, options) ->
|
|
67
73
|
if number < 0 then @all_tokens.percent.negative else @all_tokens.percent.positive
|
|
68
74
|
|
|
69
|
-
TwitterCldr.DecimalFormatter
|
|
75
|
+
class TwitterCldr.DecimalFormatter extends TwitterCldr.NumberFormatter
|
|
70
76
|
format: (number, options = {}) ->
|
|
71
77
|
try
|
|
72
78
|
super(number, options)
|
|
@@ -79,7 +85,7 @@ TwitterCldr.DecimalFormatter = class DecimalFormatter extends NumberFormatter
|
|
|
79
85
|
get_tokens: (number, options = {}) ->
|
|
80
86
|
if number < 0 then @all_tokens.decimal.negative else @all_tokens.decimal.positive
|
|
81
87
|
|
|
82
|
-
TwitterCldr.CurrencyFormatter
|
|
88
|
+
class TwitterCldr.CurrencyFormatter extends TwitterCldr.NumberFormatter
|
|
83
89
|
constructor: (options = {}) ->
|
|
84
90
|
@default_currency_symbol = "$"
|
|
85
91
|
@default_precision = 2
|
|
@@ -89,7 +95,6 @@ TwitterCldr.CurrencyFormatter = class CurrencyFormatter extends NumberFormatter
|
|
|
89
95
|
if options.currency
|
|
90
96
|
if TwitterCldr.Currencies?
|
|
91
97
|
currency = TwitterCldr.Currencies.for_code(options.currency)
|
|
92
|
-
currency ||= TwitterCldr.Currencies.for_country(options.currency)
|
|
93
98
|
currency ||= symbol: options.currency
|
|
94
99
|
else
|
|
95
100
|
currency = symbol: options.currency
|
|
@@ -106,7 +111,44 @@ TwitterCldr.CurrencyFormatter = class CurrencyFormatter extends NumberFormatter
|
|
|
106
111
|
get_tokens: (number, options = {}) ->
|
|
107
112
|
if number < 0 then @all_tokens.currency.negative else @all_tokens.currency.positive
|
|
108
113
|
|
|
109
|
-
TwitterCldr.
|
|
114
|
+
class TwitterCldr.AbbreviatedNumberFormatter extends TwitterCldr.NumberFormatter
|
|
115
|
+
NUMBER_MAX: Math.pow(10, 15)
|
|
116
|
+
NUMBER_MIN: 1000
|
|
117
|
+
|
|
118
|
+
default_format_options_for: (number) ->
|
|
119
|
+
precision: this.precision_from(number)
|
|
120
|
+
|
|
121
|
+
get_type: ->
|
|
122
|
+
"decimal"
|
|
123
|
+
|
|
124
|
+
get_key: (number) ->
|
|
125
|
+
zeroes = ("0" for i in [0...(Math.floor(number).toString().length - 1)]).join("")
|
|
126
|
+
"1#{zeroes}"
|
|
127
|
+
|
|
128
|
+
get_tokens: (number, options = {}) ->
|
|
129
|
+
type = if (number < @NUMBER_MAX) && (number >= @NUMBER_MIN) then this.get_type() else "decimal"
|
|
130
|
+
format = if type == this.get_type() then this.get_key(number) else null
|
|
131
|
+
tokens = @all_tokens[type]
|
|
132
|
+
tokens = if number < 0 then tokens.negative else tokens.positive
|
|
133
|
+
tokens = tokens[format] if format?
|
|
134
|
+
tokens
|
|
135
|
+
|
|
136
|
+
transform_number: (number) ->
|
|
137
|
+
if (number < @NUMBER_MAX) && (number >= @NUMBER_MIN)
|
|
138
|
+
sig_figs = ((parseInt(number).toString().length - 1) % 3)
|
|
139
|
+
parseInt(number.toString()[0..sig_figs])
|
|
140
|
+
else
|
|
141
|
+
number
|
|
142
|
+
|
|
143
|
+
class TwitterCldr.ShortDecimalFormatter extends TwitterCldr.AbbreviatedNumberFormatter
|
|
144
|
+
get_type: ->
|
|
145
|
+
"short_decimal"
|
|
146
|
+
|
|
147
|
+
class TwitterCldr.LongDecimalFormatter extends TwitterCldr.AbbreviatedNumberFormatter
|
|
148
|
+
get_type: ->
|
|
149
|
+
"long_decimal"
|
|
150
|
+
|
|
151
|
+
class TwitterCldr.NumberFormatter.BaseHelper
|
|
110
152
|
interpolate: (string, value, orientation = "right") ->
|
|
111
153
|
value = value.toString()
|
|
112
154
|
length = value.length
|
|
@@ -120,7 +162,7 @@ TwitterCldr.NumberFormatter.BaseHelper = class BaseHelper
|
|
|
120
162
|
|
|
121
163
|
string.replace(/#/g, "")
|
|
122
164
|
|
|
123
|
-
TwitterCldr.NumberFormatter.IntegerHelper
|
|
165
|
+
class TwitterCldr.NumberFormatter.IntegerHelper extends TwitterCldr.NumberFormatter.BaseHelper
|
|
124
166
|
constructor: (token, symbols = {}) ->
|
|
125
167
|
format = token.split('.')[0]
|
|
126
168
|
@format = this.prepare_format(format, symbols)
|
|
@@ -147,12 +189,13 @@ TwitterCldr.NumberFormatter.IntegerHelper = class IntegerHelper extends BaseHelp
|
|
|
147
189
|
(token for token in tokens when token != null).reverse().join(@separator)
|
|
148
190
|
|
|
149
191
|
parse_groups: (format) ->
|
|
150
|
-
|
|
192
|
+
index = format.lastIndexOf(',')
|
|
193
|
+
return [] unless index > 0
|
|
151
194
|
rest = format[0...index]
|
|
152
195
|
widths = [format.length - index - 1]
|
|
153
196
|
widths.push(rest.length - rest.lastIndexOf(',') - 1) if rest.lastIndexOf(',') > -1
|
|
154
|
-
widths = (width for width in widths when width != null)
|
|
155
|
-
widths.reverse()
|
|
197
|
+
widths = (width for width in widths when width != null) # compact
|
|
198
|
+
widths.reverse() # uniq
|
|
156
199
|
(widths[index] for index in [0...widths.length] when widths.indexOf(widths[index], index + 1) == -1).reverse()
|
|
157
200
|
|
|
158
201
|
chop_group: (string, size) ->
|
|
@@ -161,7 +204,7 @@ TwitterCldr.NumberFormatter.IntegerHelper = class IntegerHelper extends BaseHelp
|
|
|
161
204
|
prepare_format: (format, symbols) ->
|
|
162
205
|
format.replace(",", "").replace("+", symbols.plus_sign).replace("-", symbols.minus_sign)
|
|
163
206
|
|
|
164
|
-
TwitterCldr.NumberFormatter.FractionHelper
|
|
207
|
+
class TwitterCldr.NumberFormatter.FractionHelper extends TwitterCldr.NumberFormatter.BaseHelper
|
|
165
208
|
constructor: (token, symbols = {}) ->
|
|
166
209
|
@format = if token then token.split('.').pop() else ""
|
|
167
210
|
@decimal = symbols.decimal || "."
|
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
# Copyright 2012 Twitter, Inc
|
|
2
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
3
|
+
|
|
4
|
+
class TwitterCldr.Bidi
|
|
5
|
+
MAX_DEPTH = 62
|
|
6
|
+
|
|
7
|
+
constructor: (options = {}) ->
|
|
8
|
+
@bidi_classes = `{{{bidi_classes}}}`
|
|
9
|
+
@string_arr = options.string_arr || options.types
|
|
10
|
+
@types = options.types || []
|
|
11
|
+
@levels = []
|
|
12
|
+
@runs = []
|
|
13
|
+
@direction = options.direction
|
|
14
|
+
@default_direction = options.default_direction || "LTR"
|
|
15
|
+
@length = @types.length
|
|
16
|
+
this.run_bidi()
|
|
17
|
+
|
|
18
|
+
@bidi_class_for: (code_point) ->
|
|
19
|
+
for bidi_class, ranges of @bidi_classes
|
|
20
|
+
for range_offset, range_list of ranges
|
|
21
|
+
for range in range_list
|
|
22
|
+
start = range
|
|
23
|
+
end = start + parseInt(range_offset)
|
|
24
|
+
if (code_point >= start) && (code_point <= end)
|
|
25
|
+
return bidi_class
|
|
26
|
+
|
|
27
|
+
null
|
|
28
|
+
|
|
29
|
+
@from_string: (str, options = {}) ->
|
|
30
|
+
string_arr = TwitterCldr.Utilities.unpack_string(str)
|
|
31
|
+
options.types ||= this.compute_types(string_arr)
|
|
32
|
+
options.string_arr ||= string_arr
|
|
33
|
+
new TwitterCldr.Bidi(options)
|
|
34
|
+
|
|
35
|
+
@from_type_array: (types, options = {}) ->
|
|
36
|
+
options.types ||= types
|
|
37
|
+
new TwitterCldr.Bidi(options)
|
|
38
|
+
|
|
39
|
+
@compute_types: (arr) ->
|
|
40
|
+
TwitterCldr.Bidi.bidi_class_for(code_point) for code_point in arr
|
|
41
|
+
|
|
42
|
+
toString: ->
|
|
43
|
+
TwitterCldr.Utilities.pack_array(@string_arr)
|
|
44
|
+
|
|
45
|
+
reorder_visually: ->
|
|
46
|
+
throw "No string given!" unless @string_arr
|
|
47
|
+
|
|
48
|
+
# Do this explicitly so we can also find the maximum depth at the
|
|
49
|
+
# same time.
|
|
50
|
+
max = 0
|
|
51
|
+
lowest_odd = MAX_DEPTH + 1
|
|
52
|
+
|
|
53
|
+
for level in @levels
|
|
54
|
+
max = TwitterCldr.Utilities.max([level, max])
|
|
55
|
+
lowest_odd = TwitterCldr.Utilities.min([lowest_odd, level]) unless TwitterCldr.Utilities.is_even(level)
|
|
56
|
+
|
|
57
|
+
# Reverse the runs starting with the deepest.
|
|
58
|
+
for depth in [max...0]
|
|
59
|
+
start = 0
|
|
60
|
+
|
|
61
|
+
while start < @levels.length
|
|
62
|
+
# Find the start of a run >= DEPTH.
|
|
63
|
+
start += 1 while start < @levels.length && @levels[start] < depth
|
|
64
|
+
|
|
65
|
+
break if start == @levels.length
|
|
66
|
+
|
|
67
|
+
# Find the end of the run.
|
|
68
|
+
finish = start + 1
|
|
69
|
+
finish += 1 while finish < @levels.length && @levels[finish] >= depth
|
|
70
|
+
|
|
71
|
+
# Reverse this run.
|
|
72
|
+
for i in [0...((finish - start) / 2)]
|
|
73
|
+
tmpb = @levels[finish - i - 1]
|
|
74
|
+
@levels[finish - i - 1] = @levels[start + i]
|
|
75
|
+
@levels[start + i] = tmpb
|
|
76
|
+
|
|
77
|
+
tmpo = @string_arr[finish - i - 1]
|
|
78
|
+
@string_arr[finish - i - 1] = @string_arr[start + i]
|
|
79
|
+
@string_arr[start + i] = tmpo
|
|
80
|
+
|
|
81
|
+
# Handle the next run.
|
|
82
|
+
start = finish + 1
|
|
83
|
+
|
|
84
|
+
this
|
|
85
|
+
|
|
86
|
+
compute_paragraph_embedding_level: ->
|
|
87
|
+
# First check to see if the user supplied a directionality override.
|
|
88
|
+
if ["LTR", "RTL"].indexOf(@direction) > -1
|
|
89
|
+
if @direction == "LTR" then 0 else 1
|
|
90
|
+
else
|
|
91
|
+
# This implements rules P2 and P3.
|
|
92
|
+
# (Note that we don't need P1, as the user supplies
|
|
93
|
+
# a paragraph.)
|
|
94
|
+
for type in @types
|
|
95
|
+
return 0 if type == "L"
|
|
96
|
+
return 1 if type == "R"
|
|
97
|
+
|
|
98
|
+
if @default_direction == "LTR" then 0 else 1
|
|
99
|
+
|
|
100
|
+
compute_explicit_levels: ->
|
|
101
|
+
current_embedding = @base_embedding
|
|
102
|
+
|
|
103
|
+
# The directional override is a Character directionality
|
|
104
|
+
# constant. -1 means there is no override.
|
|
105
|
+
directional_override = -1
|
|
106
|
+
|
|
107
|
+
# The stack of pushed embeddings, and the stack pointer.
|
|
108
|
+
# Note that because the direction is inherent in the depth,
|
|
109
|
+
# and because we have a bit left over in a byte, we can encode
|
|
110
|
+
# the override, if any, directly in this value on the stack.
|
|
111
|
+
|
|
112
|
+
embedding_stack = []
|
|
113
|
+
@formatter_indices ||= []
|
|
114
|
+
sp = 0
|
|
115
|
+
|
|
116
|
+
for i in [0...@length]
|
|
117
|
+
is_ltr = false
|
|
118
|
+
is_special = true
|
|
119
|
+
is_ltr = @types[i] == "LRE" || @types[i] == "LRO"
|
|
120
|
+
|
|
121
|
+
switch @types[i]
|
|
122
|
+
when "RLE", "RLO", "LRE", "LRO"
|
|
123
|
+
new_embedding = if is_ltr
|
|
124
|
+
# Least greater even.
|
|
125
|
+
((current_embedding & ~1) + 2)
|
|
126
|
+
else
|
|
127
|
+
# Least greater odd.
|
|
128
|
+
((current_embedding + 1) | 1)
|
|
129
|
+
|
|
130
|
+
# FIXME: we don't properly handle invalid pushes.
|
|
131
|
+
if new_embedding < MAX_DEPTH
|
|
132
|
+
# The new level is valid. Push the old value.
|
|
133
|
+
# See above for a comment on the encoding here.
|
|
134
|
+
|
|
135
|
+
current_embedding |= -0x80 if (directional_override != -1)
|
|
136
|
+
embedding_stack[sp] = current_embedding
|
|
137
|
+
current_embedding = new_embedding
|
|
138
|
+
sp += 1
|
|
139
|
+
|
|
140
|
+
directional_override = if @types[i] == "LRO"
|
|
141
|
+
"L"
|
|
142
|
+
else if @types[i] == "RLO"
|
|
143
|
+
"R"
|
|
144
|
+
else
|
|
145
|
+
-1
|
|
146
|
+
|
|
147
|
+
when "PDF"
|
|
148
|
+
# FIXME: we don't properly handle a pop with a corresponding
|
|
149
|
+
# invalid push.
|
|
150
|
+
# If sp === 0, we saw a pop without a push. Just ignore it.
|
|
151
|
+
if sp > 0
|
|
152
|
+
sp -= 1
|
|
153
|
+
new_embedding = embedding_stack[sp]
|
|
154
|
+
current_embedding = new_embedding & 0x7f
|
|
155
|
+
|
|
156
|
+
directional_override = if new_embedding < 0
|
|
157
|
+
(new_embedding & 1) == 0 ? "L" : "R"
|
|
158
|
+
else
|
|
159
|
+
-1
|
|
160
|
+
|
|
161
|
+
else
|
|
162
|
+
is_special = false
|
|
163
|
+
|
|
164
|
+
@levels[i] = current_embedding
|
|
165
|
+
|
|
166
|
+
if is_special
|
|
167
|
+
# Mark this character for removal.
|
|
168
|
+
@formatter_indices.push(i)
|
|
169
|
+
else if directional_override != -1
|
|
170
|
+
@types[i] = directional_override
|
|
171
|
+
|
|
172
|
+
# Remove the formatting codes and update both the arrays
|
|
173
|
+
# and 'length'. It would be more efficient not to remove
|
|
174
|
+
# these codes, but it is also more complicated. Also, the
|
|
175
|
+
# Unicode algorithm reference does not properly describe
|
|
176
|
+
# how this is to be done -- from what I can tell, their suggestions
|
|
177
|
+
# in this area will not yield the correct results.
|
|
178
|
+
|
|
179
|
+
output = 0
|
|
180
|
+
input = 0
|
|
181
|
+
size = @formatter_indices.length
|
|
182
|
+
|
|
183
|
+
for i in [0..size]
|
|
184
|
+
next_fmt = if i == size
|
|
185
|
+
@length
|
|
186
|
+
else
|
|
187
|
+
@formatter_indices[i]
|
|
188
|
+
|
|
189
|
+
len = next_fmt - input
|
|
190
|
+
|
|
191
|
+
# Non-formatter codes are from 'input' to 'next_fmt'.
|
|
192
|
+
TwitterCldr.Utilities.arraycopy(@levels, input, @levels, output, len)
|
|
193
|
+
TwitterCldr.Utilities.arraycopy(@types, input, @types, output, len)
|
|
194
|
+
|
|
195
|
+
output += len
|
|
196
|
+
input = next_fmt + 1
|
|
197
|
+
|
|
198
|
+
@length -= @formatter_indices.length
|
|
199
|
+
|
|
200
|
+
compute_runs: ->
|
|
201
|
+
run_count = 0
|
|
202
|
+
current_embedding = @base_embedding
|
|
203
|
+
|
|
204
|
+
for i in [0...@length]
|
|
205
|
+
if @levels[i] != current_embedding
|
|
206
|
+
current_embedding = @levels[i]
|
|
207
|
+
run_count += 1
|
|
208
|
+
|
|
209
|
+
# This may be called multiple times. If so, and if
|
|
210
|
+
# the number of runs has not changed, then don't bother
|
|
211
|
+
# allocating a new array.
|
|
212
|
+
where = 0
|
|
213
|
+
last_run_start = 0
|
|
214
|
+
current_embedding = @base_embedding
|
|
215
|
+
|
|
216
|
+
for i in [0...@length]
|
|
217
|
+
if @levels[i] != current_embedding
|
|
218
|
+
@runs[where] = last_run_start
|
|
219
|
+
where += 1
|
|
220
|
+
last_run_start = i
|
|
221
|
+
current_embedding = @levels[i]
|
|
222
|
+
|
|
223
|
+
@runs[where] = last_run_start
|
|
224
|
+
|
|
225
|
+
resolve_weak_types: ->
|
|
226
|
+
run_count = @runs.length
|
|
227
|
+
previous_level = @base_embedding
|
|
228
|
+
|
|
229
|
+
for run_idx in [0...run_count]
|
|
230
|
+
start = this.get_run_start(run_idx)
|
|
231
|
+
finish = this.get_run_limit(run_idx)
|
|
232
|
+
level = this.get_run_level(run_idx) || 0
|
|
233
|
+
|
|
234
|
+
# These are the names used in the Bidi algorithm.
|
|
235
|
+
sor = if TwitterCldr.Utilities.is_even(TwitterCldr.Utilities.max([previous_level, level])) then "L" else "R"
|
|
236
|
+
|
|
237
|
+
next_level = if run_idx == (run_count - 1)
|
|
238
|
+
@base_embedding
|
|
239
|
+
else
|
|
240
|
+
this.get_run_level(run_idx + 1) || 0
|
|
241
|
+
|
|
242
|
+
eor = if TwitterCldr.Utilities.is_even(TwitterCldr.Utilities.max([level, next_level])) then "L" else "R"
|
|
243
|
+
prev_type = sor
|
|
244
|
+
prev_strong_type = sor
|
|
245
|
+
|
|
246
|
+
for i in [start...finish]
|
|
247
|
+
next_type = if i == (finish - 1) then eor else @types[i + 1]
|
|
248
|
+
|
|
249
|
+
# Rule W1: change NSM to the prevailing direction.
|
|
250
|
+
if @types[i] == "NSM"
|
|
251
|
+
@types[i] = prev_type
|
|
252
|
+
else
|
|
253
|
+
prev_type = @types[i]
|
|
254
|
+
|
|
255
|
+
# Rule W2: change EN to AN in some cases.
|
|
256
|
+
if @types[i] == "EN"
|
|
257
|
+
if prev_strong_type == "AL"
|
|
258
|
+
@types[i] = "AN"
|
|
259
|
+
else if @types[i] == "L" || @types[i] == "R" || @types[i] == "AL"
|
|
260
|
+
prev_strong_type = @types[i]
|
|
261
|
+
|
|
262
|
+
# Rule W3: change AL to R.
|
|
263
|
+
if @types[i] == "AL"
|
|
264
|
+
@types[i] = "R"
|
|
265
|
+
|
|
266
|
+
# Rule W4: handle separators between two numbers.
|
|
267
|
+
if prev_type == "EN" && next_type == "EN"
|
|
268
|
+
if @types[i] == "ES" || @types[i] == "CS"
|
|
269
|
+
@types[i] = nextType
|
|
270
|
+
else if prev_type == "AN" && next_type == "AN" && @types[i] == "CS"
|
|
271
|
+
@types[i] = next_type
|
|
272
|
+
|
|
273
|
+
# Rule W5: change a sequence of european terminators to
|
|
274
|
+
# european numbers, if they are adjacent to european numbers.
|
|
275
|
+
# We also include BN characters in this.
|
|
276
|
+
if @types[i] == "ET" || @types[i] == "BN"
|
|
277
|
+
if prev_type == "EN"
|
|
278
|
+
@types[i] = prev_type
|
|
279
|
+
else
|
|
280
|
+
# Look ahead to see if there is an EN terminating this
|
|
281
|
+
# sequence of ETs.
|
|
282
|
+
j = i + 1
|
|
283
|
+
|
|
284
|
+
while j < finish && @types[j] == "ET" || @types[j] == "BN"
|
|
285
|
+
j += 1
|
|
286
|
+
|
|
287
|
+
if j < finish && @types[j] == "EN"
|
|
288
|
+
# Change them all to EN now.
|
|
289
|
+
for k in [i...j]
|
|
290
|
+
@types[k] = "EN"
|
|
291
|
+
|
|
292
|
+
# Rule W6: separators and terminators change to ON.
|
|
293
|
+
# Again we include BN.
|
|
294
|
+
if @types[i] == "ET" || @types[i] == "CS" || @types[i] == "BN"
|
|
295
|
+
@types[i] = "ON"
|
|
296
|
+
|
|
297
|
+
# Rule W7: change european number types.
|
|
298
|
+
if prev_strong_type == "L" && @types[i] == "EN"
|
|
299
|
+
@types[i] = prev_strong_type
|
|
300
|
+
|
|
301
|
+
previous_level = level
|
|
302
|
+
|
|
303
|
+
return
|
|
304
|
+
|
|
305
|
+
get_run_count: ->
|
|
306
|
+
@runs.length
|
|
307
|
+
|
|
308
|
+
get_run_level: (which) ->
|
|
309
|
+
@levels[@runs[which]]
|
|
310
|
+
|
|
311
|
+
get_run_limit: (which) ->
|
|
312
|
+
if which == (@runs.length - 1)
|
|
313
|
+
@length
|
|
314
|
+
else
|
|
315
|
+
@runs[which + 1]
|
|
316
|
+
|
|
317
|
+
get_run_start: (which) ->
|
|
318
|
+
@runs[which]
|
|
319
|
+
|
|
320
|
+
resolve_implicit_levels: ->
|
|
321
|
+
# This implements rules I1 and I2.
|
|
322
|
+
for i in [0...@length]
|
|
323
|
+
if (@levels[i] & 1) == 0
|
|
324
|
+
if @types[i] == "R"
|
|
325
|
+
@levels[i] += 1
|
|
326
|
+
else if @types[i] == "AN" || @types[i] == "EN"
|
|
327
|
+
@levels[i] += 2
|
|
328
|
+
else
|
|
329
|
+
if @types[i] == "L" || @types[i] == "AN" || @types[i] == "EN"
|
|
330
|
+
@levels[i] += 1
|
|
331
|
+
return
|
|
332
|
+
|
|
333
|
+
resolve_neutral_types: ->
|
|
334
|
+
# This implements rules N1 and N2.
|
|
335
|
+
run_count = this.get_run_count()
|
|
336
|
+
previous_level = @base_embedding
|
|
337
|
+
|
|
338
|
+
for run in [0...run_count]
|
|
339
|
+
start = this.get_run_start(run)
|
|
340
|
+
finish = this.get_run_limit(run)
|
|
341
|
+
level = this.get_run_level(run)
|
|
342
|
+
continue unless level?
|
|
343
|
+
|
|
344
|
+
embedding_direction = if TwitterCldr.Utilities.is_even(level) then "L" else "R"
|
|
345
|
+
# These are the names used in the Bidi algorithm.
|
|
346
|
+
sor = if TwitterCldr.Utilities.is_even(TwitterCldr.Utilities.max([previous_level, level])) then "L" else "R"
|
|
347
|
+
|
|
348
|
+
next_level = if run == (run_count - 1)
|
|
349
|
+
@base_embedding
|
|
350
|
+
else
|
|
351
|
+
this.get_run_level(run + 1)
|
|
352
|
+
|
|
353
|
+
eor = if TwitterCldr.Utilities.is_even(TwitterCldr.Utilities.max([level, next_level])) then "L" else "R"
|
|
354
|
+
prev_strong = sor
|
|
355
|
+
neutral_start = -1
|
|
356
|
+
|
|
357
|
+
for i in [start..finish]
|
|
358
|
+
new_strong = -1
|
|
359
|
+
this_type = if i == finish then eor else @types[i]
|
|
360
|
+
|
|
361
|
+
switch this_type
|
|
362
|
+
when "L"
|
|
363
|
+
new_strong = "L"
|
|
364
|
+
when "R", "AN", "EN"
|
|
365
|
+
new_strong = "R"
|
|
366
|
+
when "BN", "ON", "S", "B", "WS"
|
|
367
|
+
neutral_start = i if neutral_start == -1
|
|
368
|
+
|
|
369
|
+
# If we see a strong character, update all the neutrals.
|
|
370
|
+
if new_strong != -1
|
|
371
|
+
if neutral_start != -1
|
|
372
|
+
override = if prev_strong == new_strong then prev_strong else embedding_direction
|
|
373
|
+
for j in [neutral_start...i]
|
|
374
|
+
@types[j] = override
|
|
375
|
+
|
|
376
|
+
prev_strong = new_strong
|
|
377
|
+
neutral_start = -1
|
|
378
|
+
|
|
379
|
+
previous_level = level
|
|
380
|
+
|
|
381
|
+
return
|
|
382
|
+
|
|
383
|
+
reinsert_formatting_codes: ->
|
|
384
|
+
if @formatter_indices? && @formatter_indices.length > 0
|
|
385
|
+
input = @length
|
|
386
|
+
output = @levels.length
|
|
387
|
+
|
|
388
|
+
# Process from the end as we are copying the array over itself here.
|
|
389
|
+
for index in [(@formatter_indices.length - 1)..0]
|
|
390
|
+
next_fmt = @formatter_indices[index]
|
|
391
|
+
|
|
392
|
+
# nextFmt points to a location in the original array. So,
|
|
393
|
+
# nextFmt+1 is the target of our copying. output is the location
|
|
394
|
+
# to which we last copied, thus we can derive the length of the
|
|
395
|
+
# copy from it.
|
|
396
|
+
len = output - next_fmt - 1
|
|
397
|
+
output = next_fmt
|
|
398
|
+
input -= len
|
|
399
|
+
|
|
400
|
+
# Note that we no longer need 'types' at this point, so we
|
|
401
|
+
# only edit 'levels'.
|
|
402
|
+
if next_fmt + 1 < @levels.length
|
|
403
|
+
TwitterCldr.Utilities.arraycopy(@levels, input, @levels, next_fmt + 1, len)
|
|
404
|
+
|
|
405
|
+
# Now set the level at the reinsertion point.
|
|
406
|
+
right_level = if output == @levels.length - 1
|
|
407
|
+
@base_embedding
|
|
408
|
+
else
|
|
409
|
+
if @levels[output + 1]? then @levels[output + 1] else 0
|
|
410
|
+
|
|
411
|
+
left_level = if input == 0
|
|
412
|
+
@base_embedding
|
|
413
|
+
else
|
|
414
|
+
if @levels[input]? then @levels[input] else 0
|
|
415
|
+
|
|
416
|
+
@levels[output] = TwitterCldr.Utilities.max([left_level, right_level])
|
|
417
|
+
|
|
418
|
+
@length = @levels.length
|
|
419
|
+
|
|
420
|
+
run_bidi: ->
|
|
421
|
+
@base_embedding = this.compute_paragraph_embedding_level()
|
|
422
|
+
|
|
423
|
+
this.compute_explicit_levels()
|
|
424
|
+
this.compute_runs()
|
|
425
|
+
this.resolve_weak_types()
|
|
426
|
+
this.resolve_neutral_types()
|
|
427
|
+
this.resolve_implicit_levels()
|
|
428
|
+
this.reinsert_formatting_codes()
|
|
429
|
+
|
|
430
|
+
# After resolving the implicit levels, the number
|
|
431
|
+
# of runs may have changed.
|
|
432
|
+
this.compute_runs()
|
|
433
|
+
return
|