1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4  * License, v. 2.0. If a copy of the MPL was not distributed with this
5  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 
7 #include "FluentBundle.h"
8 #include "nsContentUtils.h"
9 #include "mozilla/dom/UnionTypes.h"
10 #include "mozilla/intl/NumberFormat.h"
11 #include "mozilla/intl/DateTimeFormat.h"
12 #include "nsIInputStream.h"
13 #include "nsStringFwd.h"
14 #include "nsTArray.h"
15 
16 using namespace mozilla::dom;
17 
18 namespace mozilla {
19 namespace intl {
20 
21 class SizeableUTF8Buffer {
22  public:
23   using CharType = uint8_t;
24 
reserve(size_t size)25   bool reserve(size_t size) {
26     mBuffer.reset(reinterpret_cast<CharType*>(malloc(size)));
27     mCapacity = size;
28     return true;
29   }
30 
data()31   CharType* data() { return mBuffer.get(); }
32 
capacity() const33   size_t capacity() const { return mCapacity; }
34 
written(size_t amount)35   void written(size_t amount) { mWritten = amount; }
36 
37   size_t mWritten = 0;
38   size_t mCapacity = 0;
39 
40   struct FreePolicy {
operator ()mozilla::intl::SizeableUTF8Buffer::FreePolicy41     void operator()(const void* ptr) { free(const_cast<void*>(ptr)); }
42   };
43 
44   UniquePtr<CharType[], FreePolicy> mBuffer;
45 };
46 
NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(FluentPattern,mParent)47 NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(FluentPattern, mParent)
48 
49 NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(FluentPattern, AddRef)
50 NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(FluentPattern, Release)
51 
52 FluentPattern::FluentPattern(nsISupports* aParent, const nsACString& aId)
53     : mId(aId), mParent(aParent) {
54   MOZ_COUNT_CTOR(FluentPattern);
55 }
FluentPattern(nsISupports * aParent,const nsACString & aId,const nsACString & aAttrName)56 FluentPattern::FluentPattern(nsISupports* aParent, const nsACString& aId,
57                              const nsACString& aAttrName)
58     : mId(aId), mAttrName(aAttrName), mParent(aParent) {
59   MOZ_COUNT_CTOR(FluentPattern);
60 }
61 
WrapObject(JSContext * aCx,JS::Handle<JSObject * > aGivenProto)62 JSObject* FluentPattern::WrapObject(JSContext* aCx,
63                                     JS::Handle<JSObject*> aGivenProto) {
64   return FluentPattern_Binding::Wrap(aCx, this, aGivenProto);
65 }
66 
~FluentPattern()67 FluentPattern::~FluentPattern() { MOZ_COUNT_DTOR(FluentPattern); };
68 
69 /* FluentBundle */
70 
NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(FluentBundle,mParent)71 NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(FluentBundle, mParent)
72 
73 NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(FluentBundle, AddRef)
74 NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(FluentBundle, Release)
75 
76 FluentBundle::FluentBundle(nsISupports* aParent,
77                            UniquePtr<ffi::FluentBundleRc> aRaw)
78     : mParent(aParent), mRaw(std::move(aRaw)) {
79   MOZ_COUNT_CTOR(FluentBundle);
80 }
81 
Constructor(const dom::GlobalObject & aGlobal,const UTF8StringOrUTF8StringSequence & aLocales,const dom::FluentBundleOptions & aOptions,ErrorResult & aRv)82 already_AddRefed<FluentBundle> FluentBundle::Constructor(
83     const dom::GlobalObject& aGlobal,
84     const UTF8StringOrUTF8StringSequence& aLocales,
85     const dom::FluentBundleOptions& aOptions, ErrorResult& aRv) {
86   nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports());
87   if (!global) {
88     aRv.Throw(NS_ERROR_FAILURE);
89     return nullptr;
90   }
91 
92   bool useIsolating = aOptions.mUseIsolating;
93 
94   nsAutoCString pseudoStrategy;
95   if (aOptions.mPseudoStrategy.WasPassed()) {
96     pseudoStrategy = aOptions.mPseudoStrategy.Value();
97   }
98 
99   UniquePtr<ffi::FluentBundleRc> raw;
100 
101   if (aLocales.IsUTF8String()) {
102     const nsACString& locale = aLocales.GetAsUTF8String();
103     raw.reset(
104         ffi::fluent_bundle_new_single(&locale, useIsolating, &pseudoStrategy));
105   } else {
106     const auto& locales = aLocales.GetAsUTF8StringSequence();
107     raw.reset(ffi::fluent_bundle_new(locales.Elements(), locales.Length(),
108                                      useIsolating, &pseudoStrategy));
109   }
110 
111   if (!raw) {
112     aRv.ThrowInvalidStateError(
113         "Failed to create the FluentBundle. Check the "
114         "locales and pseudo strategy arguments.");
115     return nullptr;
116   }
117 
118   return do_AddRef(new FluentBundle(global, std::move(raw)));
119 }
120 
WrapObject(JSContext * aCx,JS::Handle<JSObject * > aGivenProto)121 JSObject* FluentBundle::WrapObject(JSContext* aCx,
122                                    JS::Handle<JSObject*> aGivenProto) {
123   return FluentBundle_Binding::Wrap(aCx, this, aGivenProto);
124 }
125 
~FluentBundle()126 FluentBundle::~FluentBundle() { MOZ_COUNT_DTOR(FluentBundle); };
127 
GetLocales(nsTArray<nsCString> & aLocales)128 void FluentBundle::GetLocales(nsTArray<nsCString>& aLocales) {
129   fluent_bundle_get_locales(mRaw.get(), &aLocales);
130 }
131 
AddResource(FluentResource & aResource,const dom::FluentBundleAddResourceOptions & aOptions)132 void FluentBundle::AddResource(
133     FluentResource& aResource,
134     const dom::FluentBundleAddResourceOptions& aOptions) {
135   bool allowOverrides = aOptions.mAllowOverrides;
136   nsTArray<nsCString> errors;
137 
138   fluent_bundle_add_resource(mRaw.get(), aResource.Raw(), allowOverrides,
139                              &errors);
140 
141   for (auto& err : errors) {
142     nsContentUtils::LogSimpleConsoleError(NS_ConvertUTF8toUTF16(err), "L10n",
143                                           false, true,
144                                           nsIScriptError::warningFlag);
145   }
146 }
147 
HasMessage(const nsACString & aId)148 bool FluentBundle::HasMessage(const nsACString& aId) {
149   return fluent_bundle_has_message(mRaw.get(), &aId);
150 }
151 
GetMessage(const nsACString & aId,Nullable<FluentMessage> & aRetVal)152 void FluentBundle::GetMessage(const nsACString& aId,
153                               Nullable<FluentMessage>& aRetVal) {
154   bool hasValue = false;
155   nsTArray<nsCString> attributes;
156   bool exists =
157       fluent_bundle_get_message(mRaw.get(), &aId, &hasValue, &attributes);
158   if (exists) {
159     FluentMessage& msg = aRetVal.SetValue();
160     if (hasValue) {
161       msg.mValue = new FluentPattern(mParent, aId);
162     }
163     for (auto& name : attributes) {
164       auto newEntry = msg.mAttributes.Entries().AppendElement(fallible);
165       newEntry->mKey = name;
166       newEntry->mValue = new FluentPattern(mParent, aId, name);
167     }
168   }
169 }
170 
extendJSArrayWithErrors(JSContext * aCx,JS::Handle<JSObject * > aErrors,nsTArray<nsCString> & aInput)171 bool extendJSArrayWithErrors(JSContext* aCx, JS::Handle<JSObject*> aErrors,
172                              nsTArray<nsCString>& aInput) {
173   uint32_t length;
174   if (NS_WARN_IF(!JS::GetArrayLength(aCx, aErrors, &length))) {
175     return false;
176   }
177 
178   for (auto& err : aInput) {
179     JS::Rooted<JS::Value> jsval(aCx);
180     if (!ToJSValue(aCx, NS_ConvertUTF8toUTF16(err), &jsval)) {
181       return false;
182     }
183     if (!JS_DefineElement(aCx, aErrors, length++, jsval, JSPROP_ENUMERATE)) {
184       return false;
185     }
186   }
187   return true;
188 }
189 
FormatPattern(JSContext * aCx,const FluentPattern & aPattern,const Nullable<L10nArgs> & aArgs,const Optional<JS::Handle<JSObject * >> & aErrors,nsACString & aRetVal,ErrorResult & aRv)190 void FluentBundle::FormatPattern(JSContext* aCx, const FluentPattern& aPattern,
191                                  const Nullable<L10nArgs>& aArgs,
192                                  const Optional<JS::Handle<JSObject*>>& aErrors,
193                                  nsACString& aRetVal, ErrorResult& aRv) {
194   nsTArray<nsCString> argIds;
195   nsTArray<ffi::FluentArgument> argValues;
196 
197   if (!aArgs.IsNull()) {
198     const L10nArgs& args = aArgs.Value();
199     for (auto& entry : args.Entries()) {
200       if (!entry.mValue.IsNull()) {
201         argIds.AppendElement(entry.mKey);
202 
203         auto& value = entry.mValue.Value();
204         if (value.IsUTF8String()) {
205           argValues.AppendElement(
206               ffi::FluentArgument::String(&value.GetAsUTF8String()));
207         } else {
208           argValues.AppendElement(
209               ffi::FluentArgument::Double_(value.GetAsDouble()));
210         }
211       }
212     }
213   }
214 
215   nsTArray<nsCString> errors;
216   bool succeeded = fluent_bundle_format_pattern(mRaw.get(), &aPattern.mId,
217                                                 &aPattern.mAttrName, &argIds,
218                                                 &argValues, &aRetVal, &errors);
219 
220   if (!succeeded) {
221     return aRv.ThrowInvalidStateError(
222         "Failed to format the FluentPattern. Likely the "
223         "pattern could not be retrieved from the bundle.");
224   }
225 
226   if (aErrors.WasPassed()) {
227     if (!extendJSArrayWithErrors(aCx, aErrors.Value(), errors)) {
228       aRv.ThrowUnknownError("Failed to add errors to an error array.");
229     }
230   }
231 }
232 
233 // FFI
234 
235 extern "C" {
FluentBuiltInNumberFormatterCreate(const nsCString * aLocale,const ffi::FluentNumberOptionsRaw * aOptions)236 ffi::RawNumberFormatter* FluentBuiltInNumberFormatterCreate(
237     const nsCString* aLocale, const ffi::FluentNumberOptionsRaw* aOptions) {
238   NumberFormatOptions options;
239   switch (aOptions->style) {
240     case ffi::FluentNumberStyleRaw::Decimal:
241       break;
242     case ffi::FluentNumberStyleRaw::Currency: {
243       std::string currency = aOptions->currency.get();
244       switch (aOptions->currency_display) {
245         case ffi::FluentNumberCurrencyDisplayStyleRaw::Symbol:
246           options.mCurrency = Some(std::make_pair(
247               currency, NumberFormatOptions::CurrencyDisplay::Symbol));
248           break;
249         case ffi::FluentNumberCurrencyDisplayStyleRaw::Code:
250           options.mCurrency = Some(std::make_pair(
251               currency, NumberFormatOptions::CurrencyDisplay::Code));
252           break;
253         case ffi::FluentNumberCurrencyDisplayStyleRaw::Name:
254           options.mCurrency = Some(std::make_pair(
255               currency, NumberFormatOptions::CurrencyDisplay::Name));
256           break;
257         default:
258           MOZ_ASSERT_UNREACHABLE();
259           break;
260       }
261     } break;
262     case ffi::FluentNumberStyleRaw::Percent:
263       options.mPercent = true;
264       break;
265     default:
266       MOZ_ASSERT_UNREACHABLE();
267       break;
268   }
269 
270   options.mUseGrouping = aOptions->use_grouping;
271   options.mMinIntegerDigits = Some(aOptions->minimum_integer_digits);
272 
273   if (aOptions->minimum_significant_digits >= 0 ||
274       aOptions->maximum_significant_digits >= 0) {
275     options.mSignificantDigits =
276         Some(std::make_pair(aOptions->minimum_significant_digits,
277                             aOptions->maximum_significant_digits));
278   } else {
279     options.mFractionDigits = Some(std::make_pair(
280         aOptions->minimum_fraction_digits, aOptions->maximum_fraction_digits));
281   }
282 
283   Result<UniquePtr<NumberFormat>, NumberFormat::FormatError> result =
284       NumberFormat::TryCreate(aLocale->get(), options);
285 
286   MOZ_ASSERT(result.isOk());
287 
288   if (result.isOk()) {
289     return reinterpret_cast<ffi::RawNumberFormatter*>(
290         result.unwrap().release());
291   }
292 
293   return nullptr;
294 }
295 
FluentBuiltInNumberFormatterFormat(const ffi::RawNumberFormatter * aFormatter,double input,size_t * aOutCount,size_t * aOutCapacity)296 uint8_t* FluentBuiltInNumberFormatterFormat(
297     const ffi::RawNumberFormatter* aFormatter, double input, size_t* aOutCount,
298     size_t* aOutCapacity) {
299   const NumberFormat* nf = reinterpret_cast<const NumberFormat*>(aFormatter);
300 
301   SizeableUTF8Buffer buffer;
302   if (nf->format(input, buffer).isOk()) {
303     *aOutCount = buffer.mWritten;
304     *aOutCapacity = buffer.mCapacity;
305     return buffer.mBuffer.release();
306   }
307 
308   return nullptr;
309 }
310 
FluentBuiltInNumberFormatterDestroy(ffi::RawNumberFormatter * aFormatter)311 void FluentBuiltInNumberFormatterDestroy(ffi::RawNumberFormatter* aFormatter) {
312   delete reinterpret_cast<NumberFormat*>(aFormatter);
313 }
314 
315 /* DateTime */
316 
GetStyle(ffi::FluentDateTimeStyle aStyle)317 static DateTimeStyle GetStyle(ffi::FluentDateTimeStyle aStyle) {
318   switch (aStyle) {
319     case ffi::FluentDateTimeStyle::Full:
320       return DateTimeStyle::Full;
321     case ffi::FluentDateTimeStyle::Long:
322       return DateTimeStyle::Long;
323     case ffi::FluentDateTimeStyle::Medium:
324       return DateTimeStyle::Medium;
325     case ffi::FluentDateTimeStyle::Short:
326       return DateTimeStyle::Short;
327     case ffi::FluentDateTimeStyle::None:
328       return DateTimeStyle::None;
329     default:
330       MOZ_ASSERT_UNREACHABLE("Unsupported date time style.");
331       return DateTimeStyle::None;
332   }
333 }
334 
FluentBuiltInDateTimeFormatterCreate(const nsCString * aLocale,const ffi::FluentDateTimeOptionsRaw * aOptions)335 ffi::RawDateTimeFormatter* FluentBuiltInDateTimeFormatterCreate(
336     const nsCString* aLocale, const ffi::FluentDateTimeOptionsRaw* aOptions) {
337   if (aOptions->date_style == ffi::FluentDateTimeStyle::None &&
338       aOptions->time_style == ffi::FluentDateTimeStyle::None &&
339       !aOptions->skeleton.IsEmpty()) {
340     auto result = DateTimeFormat::TryCreateFromSkeleton(
341         Span(aLocale->get(), aLocale->Length()),
342         Span(aOptions->skeleton.get(), aOptions->skeleton.Length()));
343     if (result.isErr()) {
344       MOZ_ASSERT_UNREACHABLE("There was an error in DateTimeFormat");
345       return nullptr;
346     }
347 
348     return reinterpret_cast<ffi::RawDateTimeFormatter*>(
349         result.unwrap().release());
350   }
351 
352   auto result = DateTimeFormat::TryCreateFromStyle(
353       Span(aLocale->get(), aLocale->Length()), GetStyle(aOptions->date_style),
354       GetStyle(aOptions->time_style));
355 
356   if (result.isErr()) {
357     MOZ_ASSERT_UNREACHABLE("There was an error in DateTimeFormat");
358     return nullptr;
359   }
360 
361   return reinterpret_cast<ffi::RawDateTimeFormatter*>(
362       result.unwrap().release());
363 }
364 
FluentBuiltInDateTimeFormatterFormat(const ffi::RawDateTimeFormatter * aFormatter,double aUnixEpoch,uint32_t * aOutCount)365 uint8_t* FluentBuiltInDateTimeFormatterFormat(
366     const ffi::RawDateTimeFormatter* aFormatter, double aUnixEpoch,
367     uint32_t* aOutCount) {
368   const auto* dtFormat = reinterpret_cast<const DateTimeFormat*>(aFormatter);
369 
370   SizeableUTF8Buffer buffer;
371   dtFormat->TryFormat(aUnixEpoch, buffer).unwrap();
372 
373   *aOutCount = buffer.mWritten;
374 
375   return buffer.mBuffer.release();
376 }
377 
FluentBuiltInDateTimeFormatterDestroy(ffi::RawDateTimeFormatter * aFormatter)378 void FluentBuiltInDateTimeFormatterDestroy(
379     ffi::RawDateTimeFormatter* aFormatter) {
380   delete reinterpret_cast<const DateTimeFormat*>(aFormatter);
381 }
382 }
383 
384 }  // namespace intl
385 }  // namespace mozilla
386