Tidying.
[libdcp.git] / src / language_tag.cc
1 /*
2     Copyright (C) 2020-2021 Carl Hetherington <cth@carlh.net>
3
4     This file is part of libdcp.
5
6     libdcp is free software; you can redistribute it and/or modify
7     it under the terms of the GNU General Public License as published by
8     the Free Software Foundation; either version 2 of the License, or
9     (at your option) any later version.
10
11     libdcp is distributed in the hope that it will be useful,
12     but WITHOUT ANY WARRANTY; without even the implied warranty of
13     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14     GNU General Public License for more details.
15
16     You should have received a copy of the GNU General Public License
17     along with libdcp.  If not, see <http://www.gnu.org/licenses/>.
18
19     In addition, as a special exception, the copyright holders give
20     permission to link the code of portions of this program with the
21     OpenSSL library under certain conditions as described in each
22     individual source file, and distribute linked combinations
23     including the two.
24
25     You must obey the GNU General Public License in all respects
26     for all of the code used other than OpenSSL.  If you modify
27     file(s) with this exception, you may extend this exception to your
28     version of the file(s), but you are not obligated to do so.  If you
29     do not wish to do so, delete this exception statement from your
30     version.  If you delete this exception statement from all source
31     files in the program, then also delete it here.
32 */
33
34
35 /** @file  src/language_tag.cc
36  *  @brief LanguageTag class
37  */
38
39
40 #include "compose.hpp"
41 #include "dcp_assert.h"
42 #include "exceptions.h"
43 #include "language_tag.h"
44 #include <boost/algorithm/string.hpp>
45 #include <string>
46
47
48 using std::make_pair;
49 using std::ostream;
50 using std::pair;
51 using std::string;
52 using std::vector;
53 using boost::optional;
54 using boost::algorithm::trim;
55 using namespace dcp;
56
57
58 static vector<LanguageTag::SubtagData> language_list;
59 static vector<LanguageTag::SubtagData> variant_list;
60 static vector<LanguageTag::SubtagData> region_list;
61 static vector<LanguageTag::SubtagData> script_list;
62 static vector<LanguageTag::SubtagData> extlang_list;
63
64
65 static
66 optional<LanguageTag::SubtagData>
67 find_in_list (vector<LanguageTag::SubtagData> const& list, string subtag)
68 {
69         for (auto const& i: list) {
70                 if (boost::iequals(i.subtag, subtag)) {
71                         return i;
72                 }
73         }
74
75         return {};
76 }
77
78
79 LanguageTag::Subtag::Subtag (string subtag, SubtagType type)
80         : _subtag (subtag)
81 {
82         if (!get_subtag_data(type, subtag)) {
83                 throw LanguageTagError(String::compose("Unknown %1 string %2", subtag_type_name(type), subtag));
84         }
85 }
86
87
88 LanguageTag::LanguageTag (string tag)
89 {
90         vector<string> parts;
91         boost::split (parts, tag, boost::is_any_of("-"));
92         if (parts.empty()) {
93                 throw LanguageTagError (String::compose("Could not parse language tag %1", tag));
94         }
95
96         vector<string>::size_type p = 0;
97         _language = LanguageSubtag (parts[p]);
98         ++p;
99
100         if (p == parts.size()) {
101                 return;
102         }
103
104         try {
105                 _script = ScriptSubtag (parts[p]);
106                 ++p;
107         } catch (...) {}
108
109         if (p == parts.size()) {
110                 return;
111         }
112
113         try {
114                 _region = RegionSubtag (parts[p]);
115                 ++p;
116         } catch (...) {}
117
118         if (p == parts.size()) {
119                 return;
120         }
121
122         try {
123                 while (true) {
124                         _variants.push_back (VariantSubtag(parts[p]));
125                         ++p;
126                         if (p == parts.size()) {
127                                 return;
128                         }
129                 }
130         } catch (...) {}
131
132         try {
133                 while (true) {
134                         _extlangs.push_back (ExtlangSubtag(parts[p]));
135                         ++p;
136                         if (p == parts.size()) {
137                                 return;
138                         }
139                 }
140         } catch (...) {}
141
142         if (p < parts.size()) {
143                 throw LanguageTagError (String::compose("Unrecognised subtag %1", parts[p]));
144         }
145 }
146
147
148 string
149 LanguageTag::to_string () const
150 {
151         if (!_language) {
152                 throw LanguageTagError("No language set up");
153         }
154
155         auto s = _language->subtag();
156
157         if (_script) {
158                 s += "-" + _script->subtag();
159         }
160
161         if (_region) {
162                 s += "-" + _region->subtag();
163         }
164
165         for (auto i: _variants) {
166                 s += "-" + i.subtag();
167         }
168
169         for (auto i: _extlangs) {
170                 s += "-" + i.subtag();
171         }
172
173         return s;
174 }
175
176
177 void
178 LanguageTag::set_language (LanguageSubtag language)
179 {
180         _language = language;
181 }
182
183
184 void
185 LanguageTag::set_script (ScriptSubtag script)
186 {
187         _script = script;
188 }
189
190
191 void
192 LanguageTag::set_region (RegionSubtag region)
193 {
194         _region = region;
195 }
196
197
198 void
199 LanguageTag::add_variant (VariantSubtag variant)
200 {
201         if (find(_variants.begin(), _variants.end(), variant) != _variants.end()) {
202                 throw LanguageTagError (String::compose("Duplicate Variant subtag %1", variant.subtag()));
203         }
204
205         _variants.push_back (variant);
206 }
207
208
209 template <class T>
210 void
211 check_for_duplicates (vector<T> const& subtags, dcp::LanguageTag::SubtagType type)
212 {
213         vector<T> sorted = subtags;
214         sort (sorted.begin(), sorted.end());
215         optional<T> last;
216         for (auto const& i: sorted) {
217                 if (last && i == *last) {
218                         throw LanguageTagError (String::compose("Duplicate %1 subtag %2", dcp::LanguageTag::subtag_type_name(type), i.subtag()));
219                 }
220                 last = i;
221         }
222 }
223
224
225 void
226 LanguageTag::set_variants (vector<VariantSubtag> variants)
227 {
228         check_for_duplicates (variants, SubtagType::VARIANT);
229         _variants = variants;
230 }
231
232
233 void
234 LanguageTag::add_extlang (ExtlangSubtag extlang)
235 {
236         if (find(_extlangs.begin(), _extlangs.end(), extlang) != _extlangs.end()) {
237                 throw LanguageTagError (String::compose("Duplicate Extlang subtag %1", extlang.subtag()));
238         }
239
240         _extlangs.push_back (extlang);
241 }
242
243
244 void
245 LanguageTag::set_extlangs (vector<ExtlangSubtag> extlangs)
246 {
247         check_for_duplicates (extlangs, SubtagType::EXTLANG);
248         _extlangs = extlangs;
249 }
250
251
252 string
253 LanguageTag::description () const
254 {
255         if (!_language) {
256                 throw LanguageTagError("No language set up");
257         }
258
259         string d;
260
261         for (auto const& i: _variants) {
262                 optional<SubtagData> variant = get_subtag_data (SubtagType::VARIANT, i.subtag());
263                 DCP_ASSERT (variant);
264                 d += variant->description + " dialect of ";
265         }
266
267         auto language = get_subtag_data (SubtagType::LANGUAGE, _language->subtag());
268         DCP_ASSERT (language);
269         d += language->description;
270
271         if (_script) {
272                 auto script = get_subtag_data (SubtagType::SCRIPT, _script->subtag());
273                 DCP_ASSERT (script);
274                 d += " written using the " + script->description + " script";
275         }
276
277         if (_region) {
278                 auto region = get_subtag_data (SubtagType::REGION, _region->subtag());
279                 DCP_ASSERT (region);
280                 d += " for " + region->description;
281         }
282
283         for (auto const& i: _extlangs) {
284                 auto extlang = get_subtag_data (SubtagType::EXTLANG, i.subtag());
285                 DCP_ASSERT (extlang);
286                 d += ", " + extlang->description;
287         }
288
289         return d;
290 }
291
292
293 vector<LanguageTag::SubtagData> const &
294 LanguageTag::get_all (SubtagType type)
295 {
296         switch (type) {
297         case SubtagType::LANGUAGE:
298                 return language_list;
299         case SubtagType::SCRIPT:
300                 return script_list;
301         case SubtagType::REGION:
302                 return region_list;
303         case SubtagType::VARIANT:
304                 return variant_list;
305         case SubtagType::EXTLANG:
306                 return extlang_list;
307         }
308
309         return language_list;
310 }
311
312
313 string
314 LanguageTag::subtag_type_name (SubtagType type)
315 {
316         switch (type) {
317                 case SubtagType::LANGUAGE:
318                         return "Language";
319                 case SubtagType::SCRIPT:
320                         return "Script";
321                 case SubtagType::REGION:
322                         return "Region";
323                 case SubtagType::VARIANT:
324                         return "Variant";
325                 case SubtagType::EXTLANG:
326                         return "Extended";
327         }
328
329         return {};
330 }
331
332 bool
333 dcp::LanguageTag::VariantSubtag::operator== (VariantSubtag const & other) const
334 {
335         return subtag() == other.subtag();
336 }
337
338
339 bool
340 dcp::LanguageTag::VariantSubtag::operator< (VariantSubtag const & other) const
341 {
342         return subtag() < other.subtag();
343 }
344
345
346 bool
347 dcp::LanguageTag::ExtlangSubtag::operator== (ExtlangSubtag const & other) const
348 {
349         return subtag() == other.subtag();
350 }
351
352
353 bool
354 dcp::LanguageTag::ExtlangSubtag::operator< (ExtlangSubtag const & other) const
355 {
356         return subtag() < other.subtag();
357 }
358
359
360 bool
361 dcp::operator== (dcp::LanguageTag const& a, dcp::LanguageTag const& b)
362 {
363         return a.to_string() == b.to_string();
364 }
365
366
367 ostream&
368 dcp::operator<< (ostream& os, dcp::LanguageTag const& tag)
369 {
370         os << tag.to_string();
371         return os;
372 }
373
374
375 vector<pair<LanguageTag::SubtagType, LanguageTag::SubtagData> >
376 LanguageTag::subtags () const
377 {
378         vector<pair<SubtagType, SubtagData>> s;
379
380         if (_language) {
381                 s.push_back (make_pair(SubtagType::LANGUAGE, *get_subtag_data(SubtagType::LANGUAGE, _language->subtag())));
382         }
383
384         if (_script) {
385                 s.push_back (make_pair(SubtagType::SCRIPT, *get_subtag_data(SubtagType::SCRIPT, _script->subtag())));
386         }
387
388         if (_region) {
389                 s.push_back (make_pair(SubtagType::REGION, *get_subtag_data(SubtagType::REGION, _region->subtag())));
390         }
391
392         for (auto const& i: _variants) {
393                 s.push_back (make_pair(SubtagType::VARIANT, *get_subtag_data(SubtagType::VARIANT, i.subtag())));
394         }
395
396         for (auto const& i: _extlangs) {
397                 s.push_back (make_pair(SubtagType::EXTLANG, *get_subtag_data(SubtagType::EXTLANG, i.subtag())));
398         }
399
400         return s;
401 }
402
403
404 optional<LanguageTag::SubtagData>
405 LanguageTag::get_subtag_data (LanguageTag::SubtagType type, string subtag)
406 {
407         switch (type) {
408         case SubtagType::LANGUAGE:
409                 return find_in_list(language_list, subtag);
410         case SubtagType::SCRIPT:
411                 return find_in_list(script_list, subtag);
412         case SubtagType::REGION:
413                 return find_in_list(region_list, subtag);
414         case SubtagType::VARIANT:
415                 return find_in_list(variant_list, subtag);
416         case SubtagType::EXTLANG:
417                 return find_in_list(extlang_list, subtag);
418         }
419
420         return {};
421 }
422
423
424 optional<string>
425 LanguageTag::get_subtag_description (LanguageTag::SubtagType type, string subtag)
426 {
427         auto data = get_subtag_data (type, subtag);
428         if (!data) {
429                 return {};
430         }
431
432         return data->description;
433 }
434
435
436 void
437 load_language_tag_list (boost::filesystem::path tags_directory, string name, vector<LanguageTag::SubtagData>& list)
438 {
439         auto f = fopen_boost (tags_directory / name, "r");
440         if (!f) {
441                 throw FileError ("Could not open tags file", tags_directory / name, errno);
442         }
443         char buffer[512];
444
445         int i = 0;
446         while (!feof(f)) {
447                 char* r = fgets (buffer, sizeof(buffer), f);
448                 if (r == 0) {
449                         break;
450                 }
451                 string a = buffer;
452                 trim (a);
453                 r = fgets (buffer, sizeof(buffer), f);
454                 if (r == 0) {
455                         fclose (f);
456                         throw FileError ("Bad tags file", tags_directory / name, -1);
457                 }
458                 string b = buffer;
459                 trim (b);
460                 list.push_back (LanguageTag::SubtagData(a, b));
461                 ++i;
462         }
463
464         fclose (f);
465 }
466
467
468 void
469 dcp::load_language_tag_lists (boost::filesystem::path tags_directory)
470 {
471         load_language_tag_list (tags_directory, "language", language_list);
472         load_language_tag_list (tags_directory, "variant", variant_list);
473         load_language_tag_list (tags_directory, "region", region_list);
474         load_language_tag_list (tags_directory, "script", script_list);
475         load_language_tag_list (tags_directory, "extlang", extlang_list);
476 }
477
478