Bv2.1 7.2.3: Check that subtitle <StartTime> exists and is 0.
[libdcp.git] / src / language_tag.cc
1 /*
2     Copyright (C) 2020 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 #include "compose.hpp"
36 #include "dcp_assert.h"
37 #include "exceptions.h"
38 #include "language_tag.h"
39 #include <boost/algorithm/string.hpp>
40 #include <boost/foreach.hpp>
41 #include <string>
42
43
44 using std::make_pair;
45 using std::ostream;
46 using std::pair;
47 using std::string;
48 using std::vector;
49 using boost::optional;
50 using boost::algorithm::trim;
51 using namespace dcp;
52
53
54
55
56 static vector<LanguageTag::SubtagData> language_list;
57 static vector<LanguageTag::SubtagData> variant_list;
58 static vector<LanguageTag::SubtagData> region_list;
59 static vector<LanguageTag::SubtagData> script_list;
60 static vector<LanguageTag::SubtagData> extlang_list;
61
62
63
64 static
65 optional<LanguageTag::SubtagData>
66 find_in_list (vector<LanguageTag::SubtagData> const& list, string subtag)
67 {
68         BOOST_FOREACH (LanguageTag::SubtagData const& i, list) {
69                 if (boost::iequals(i.subtag, subtag)) {
70                         return i;
71                 }
72         }
73
74         return optional<LanguageTag::SubtagData>();
75 }
76
77
78 LanguageTag::Subtag::Subtag (string subtag, SubtagType type)
79         : _subtag (subtag)
80 {
81         if (!get_subtag_data(type, subtag)) {
82                 throw LanguageTagError(String::compose("Unknown %1 string %2", subtag_type_name(type), subtag));
83         }
84 }
85
86
87 LanguageTag::LanguageTag (string tag)
88 {
89         vector<string> parts;
90         boost::split (parts, tag, boost::is_any_of("-"));
91         if (parts.empty()) {
92                 throw LanguageTagError (String::compose("Could not parse language tag %1", tag));
93         }
94
95         vector<string>::size_type p = 0;
96         _language = LanguageSubtag (parts[p]);
97         ++p;
98
99         if (p == parts.size()) {
100                 return;
101         }
102
103         try {
104                 _script = ScriptSubtag (parts[p]);
105                 ++p;
106         } catch (...) {}
107
108         if (p == parts.size()) {
109                 return;
110         }
111
112         try {
113                 _region = RegionSubtag (parts[p]);
114                 ++p;
115         } catch (...) {}
116
117         if (p == parts.size()) {
118                 return;
119         }
120
121         try {
122                 while (true) {
123                         _variants.push_back (VariantSubtag(parts[p]));
124                         ++p;
125                         if (p == parts.size()) {
126                                 return;
127                         }
128                 }
129         } catch (...) {}
130
131         try {
132                 while (true) {
133                         _extlangs.push_back (ExtlangSubtag(parts[p]));
134                         ++p;
135                         if (p == parts.size()) {
136                                 return;
137                         }
138                 }
139         } catch (...) {}
140
141         if (p < parts.size()) {
142                 throw LanguageTagError (String::compose("Unrecognised subtag %1", parts[p]));
143         }
144 }
145
146
147 string
148 LanguageTag::to_string () const
149 {
150         if (!_language) {
151                 throw LanguageTagError("No language set up");
152         }
153
154         string s = _language->subtag();
155
156         if (_script) {
157                 s += "-" + _script->subtag();
158         }
159
160         if (_region) {
161                 s += "-" + _region->subtag();
162         }
163
164         BOOST_FOREACH (VariantSubtag i, _variants) {
165                 s += "-" + i.subtag();
166         }
167
168         BOOST_FOREACH (ExtlangSubtag i, _extlangs) {
169                 s += "-" + i.subtag();
170         }
171
172         return s;
173 }
174
175
176 void
177 LanguageTag::set_language (LanguageSubtag language)
178 {
179         _language = language;
180 }
181
182
183 void
184 LanguageTag::set_script (ScriptSubtag script)
185 {
186         _script = script;
187 }
188
189
190 void
191 LanguageTag::set_region (RegionSubtag region)
192 {
193         _region = region;
194 }
195
196
197 void
198 LanguageTag::add_variant (VariantSubtag variant)
199 {
200         if (find(_variants.begin(), _variants.end(), variant) != _variants.end()) {
201                 throw LanguageTagError (String::compose("Duplicate Variant subtag %1", variant.subtag()));
202         }
203
204         _variants.push_back (variant);
205 }
206
207
208 template <class T>
209 void
210 check_for_duplicates (vector<T> const& subtags, dcp::LanguageTag::SubtagType type)
211 {
212         vector<T> sorted = subtags;
213         sort (sorted.begin(), sorted.end());
214         optional<T> last;
215         BOOST_FOREACH (T const& i, sorted) {
216                 if (last && i == *last) {
217                         throw LanguageTagError (String::compose("Duplicate %1 subtag %2", dcp::LanguageTag::subtag_type_name(type), i.subtag()));
218                 }
219                 last = i;
220         }
221 }
222
223
224 void
225 LanguageTag::set_variants (vector<VariantSubtag> variants)
226 {
227         check_for_duplicates (variants, VARIANT);
228         _variants = variants;
229 }
230
231
232 void
233 LanguageTag::add_extlang (ExtlangSubtag extlang)
234 {
235         if (find(_extlangs.begin(), _extlangs.end(), extlang) != _extlangs.end()) {
236                 throw LanguageTagError (String::compose("Duplicate Extlang subtag %1", extlang.subtag()));
237         }
238
239         _extlangs.push_back (extlang);
240 }
241
242
243 void
244 LanguageTag::set_extlangs (vector<ExtlangSubtag> extlangs)
245 {
246         check_for_duplicates (extlangs, EXTLANG);
247         _extlangs = extlangs;
248 }
249
250
251 string
252 LanguageTag::description () const
253 {
254         if (!_language) {
255                 throw LanguageTagError("No language set up");
256         }
257
258         string d;
259
260         BOOST_FOREACH (VariantSubtag const& i, _variants) {
261                 optional<SubtagData> variant = get_subtag_data (VARIANT, i.subtag());
262                 DCP_ASSERT (variant);
263                 d += variant->description + " dialect of ";
264         }
265
266         optional<SubtagData> language = get_subtag_data (LANGUAGE, _language->subtag());
267         DCP_ASSERT (language);
268         d += language->description;
269
270         if (_script) {
271                 optional<SubtagData> script = get_subtag_data (SCRIPT, _script->subtag());
272                 DCP_ASSERT (script);
273                 d += " written using the " + script->description + " script";
274         }
275
276         if (_region) {
277                 optional<SubtagData> region = get_subtag_data (REGION, _region->subtag());
278                 DCP_ASSERT (region);
279                 d += " for " + region->description;
280         }
281
282         BOOST_FOREACH (ExtlangSubtag const& i, _extlangs) {
283                 optional<SubtagData> extlang = get_subtag_data (EXTLANG, i.subtag());
284                 DCP_ASSERT (extlang);
285                 d += ", " + extlang->description;
286         }
287
288         return d;
289 }
290
291
292 vector<LanguageTag::SubtagData> const &
293 LanguageTag::get_all (SubtagType type)
294 {
295         switch (type) {
296         case LANGUAGE:
297                 return language_list;
298         case SCRIPT:
299                 return script_list;
300         case REGION:
301                 return region_list;
302         case VARIANT:
303                 return variant_list;
304         case EXTLANG:
305                 return extlang_list;
306         }
307
308         return language_list;
309 }
310
311
312 string
313 LanguageTag::subtag_type_name (SubtagType type)
314 {
315         switch (type) {
316                 case LANGUAGE:
317                         return "Language";
318                 case SCRIPT:
319                         return "Script";
320                 case REGION:
321                         return "Region";
322                 case VARIANT:
323                         return "Variant";
324                 case EXTLANG:
325                         return "Extended";
326         }
327
328         return "";
329 }
330
331 bool
332 dcp::LanguageTag::VariantSubtag::operator== (VariantSubtag const & other) const
333 {
334         return subtag() == other.subtag();
335 }
336
337
338 bool
339 dcp::LanguageTag::VariantSubtag::operator< (VariantSubtag const & other) const
340 {
341         return subtag() < other.subtag();
342 }
343
344
345 bool
346 dcp::LanguageTag::ExtlangSubtag::operator== (ExtlangSubtag const & other) const
347 {
348         return subtag() == other.subtag();
349 }
350
351
352 bool
353 dcp::LanguageTag::ExtlangSubtag::operator< (ExtlangSubtag const & other) const
354 {
355         return subtag() < other.subtag();
356 }
357
358
359 bool
360 dcp::operator== (dcp::LanguageTag const& a, dcp::LanguageTag const& b)
361 {
362         return a.to_string() == b.to_string();
363 }
364
365
366 ostream&
367 dcp::operator<< (ostream& os, dcp::LanguageTag const& tag)
368 {
369         os << tag.to_string();
370         return os;
371 }
372
373
374 vector<pair<LanguageTag::SubtagType, LanguageTag::SubtagData> >
375 LanguageTag::subtags () const
376 {
377         vector<pair<SubtagType, SubtagData> > s;
378
379         if (_language) {
380                 s.push_back (make_pair(LANGUAGE, *get_subtag_data(LANGUAGE, _language->subtag())));
381         }
382
383         if (_script) {
384                 s.push_back (make_pair(SCRIPT, *get_subtag_data(SCRIPT, _script->subtag())));
385         }
386
387         if (_region) {
388                 s.push_back (make_pair(REGION, *get_subtag_data(REGION, _region->subtag())));
389         }
390
391         BOOST_FOREACH (VariantSubtag const& i, _variants) {
392                 s.push_back (make_pair(VARIANT, *get_subtag_data(VARIANT, i.subtag())));
393         }
394
395         BOOST_FOREACH (ExtlangSubtag const& i, _extlangs) {
396                 s.push_back (make_pair(EXTLANG, *get_subtag_data(EXTLANG, i.subtag())));
397         }
398
399         return s;
400 }
401
402
403 optional<LanguageTag::SubtagData>
404 LanguageTag::get_subtag_data (LanguageTag::SubtagType type, string subtag)
405 {
406         switch (type) {
407         case dcp::LanguageTag::LANGUAGE:
408                 return find_in_list(language_list, subtag);
409         case dcp::LanguageTag::SCRIPT:
410                 return find_in_list(script_list, subtag);
411         case dcp::LanguageTag::REGION:
412                 return find_in_list(region_list, subtag);
413         case dcp::LanguageTag::VARIANT:
414                 return find_in_list(variant_list, subtag);
415         case dcp::LanguageTag::EXTLANG:
416                 return find_in_list(extlang_list, subtag);
417         }
418
419         return optional<LanguageTag::SubtagData>();
420 }
421
422
423 optional<string>
424 LanguageTag::get_subtag_description (LanguageTag::SubtagType type, string subtag)
425 {
426         optional<SubtagData> data = get_subtag_data (type, subtag);
427         if (!data) {
428                 return optional<string>();
429         }
430
431         return data->description;
432 }
433
434
435 void
436 load_language_tag_list (boost::filesystem::path tags_directory, string name, vector<LanguageTag::SubtagData>& list)
437 {
438         FILE* f = fopen_boost (tags_directory / name, "r");
439         if (!f) {
440                 throw FileError ("Could not open tags file", tags_directory / name, errno);
441         }
442         char buffer[512];
443
444         int i = 0;
445         while (!feof(f)) {
446                 char* r = fgets (buffer, sizeof(buffer), f);
447                 if (r == 0) {
448                         break;
449                 }
450                 string a = buffer;
451                 trim (a);
452                 r = fgets (buffer, sizeof(buffer), f);
453                 if (r == 0) {
454                         fclose (f);
455                         throw FileError ("Bad tags file", tags_directory / name, -1);
456                 }
457                 string b = buffer;
458                 trim (b);
459                 list.push_back (LanguageTag::SubtagData(a, b));
460                 ++i;
461         }
462
463         fclose (f);
464 }
465
466
467 void
468 dcp::load_language_tag_lists (boost::filesystem::path tags_directory)
469 {
470         load_language_tag_list (tags_directory, "language", language_list);
471         load_language_tag_list (tags_directory, "variant", variant_list);
472         load_language_tag_list (tags_directory, "region", region_list);
473         load_language_tag_list (tags_directory, "script", script_list);
474         load_language_tag_list (tags_directory, "extlang", extlang_list);
475 }
476
477