jekyll-open-sdg-plugins 1.6.0.pre.beta1 → 1.7.0.pre.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (30) hide show
  1. checksums.yaml +4 -4
  2. data/.editorconfig +16 -16
  3. data/.github/workflows/test-pull-requests.yml +17 -17
  4. data/.gitignore +6 -6
  5. data/LICENSE +21 -21
  6. data/Makefile +33 -33
  7. data/README.md +7 -7
  8. data/jekyll-open-sdg-plugins.gemspec +18 -18
  9. data/lib/jekyll-open-sdg-plugins/backwards_compatibility.rb +64 -61
  10. data/lib/jekyll-open-sdg-plugins/create_goals.rb +85 -85
  11. data/lib/jekyll-open-sdg-plugins/create_indicators.rb +206 -206
  12. data/lib/jekyll-open-sdg-plugins/create_pages.rb +135 -135
  13. data/lib/jekyll-open-sdg-plugins/fetch_remote_data.rb +188 -188
  14. data/lib/jekyll-open-sdg-plugins/helpers.rb +132 -132
  15. data/lib/jekyll-open-sdg-plugins/metadata_schema_to_config.rb +72 -72
  16. data/lib/jekyll-open-sdg-plugins/schema-indicator-config.json +787 -709
  17. data/lib/jekyll-open-sdg-plugins/schema-site-config.json +1652 -1617
  18. data/lib/jekyll-open-sdg-plugins/sdg_variables.rb +614 -549
  19. data/lib/jekyll-open-sdg-plugins/search_index.rb +102 -102
  20. data/lib/jekyll-open-sdg-plugins/site_configuration.rb +73 -52
  21. data/lib/jekyll-open-sdg-plugins/translate_date.rb +122 -122
  22. data/lib/jekyll-open-sdg-plugins/translate_key.rb +20 -20
  23. data/lib/jekyll-open-sdg-plugins/translate_metadata_field.rb +111 -111
  24. data/lib/jekyll-open-sdg-plugins/validate_indicator_config.rb +52 -52
  25. data/lib/jekyll-open-sdg-plugins/validate_site_config.rb +34 -34
  26. data/lib/jekyll-open-sdg-plugins/version.rb +3 -3
  27. data/lib/jekyll-open-sdg-plugins.rb +18 -18
  28. data/tests/Gemfile +7 -7
  29. data/tests/_config.yml +168 -168
  30. metadata +3 -3
@@ -1,549 +1,614 @@
1
- require "jekyll"
2
- require_relative "helpers"
3
-
4
- module JekyllOpenSdgPlugins
5
- class SDGVariables < Jekyll::Generator
6
- safe true
7
- priority :low
8
-
9
- # Get a goal number from an indicator number.
10
- def get_goal_number(indicator_number)
11
- parts = indicator_number.split('.')
12
- parts[0]
13
- end
14
-
15
- # Get a target number from an indicator number.
16
- def get_target_number(indicator_number)
17
- parts = indicator_number.split('.')
18
- if parts.length() < 2
19
- indicator_number
20
- else
21
- parts[0] + '.' + parts[1]
22
- end
23
- end
24
-
25
- # Is this string numeric?
26
- def is_number? string
27
- true if Float(string) rescue false
28
- end
29
-
30
- # Make any goal/target/indicator number suitable for use in sorting.
31
- def get_sort_order(number)
32
- if number.is_a? Numeric
33
- number = number.to_s
34
- end
35
- sort_order = ''
36
- parts = number.split('.')
37
- parts.each do |part|
38
- if part.length == 1
39
- if is_number?(part)
40
- part = '0' + part
41
- else
42
- part = part + part
43
- end
44
- end
45
- sort_order += part
46
- end
47
- sort_order
48
- end
49
-
50
- # Get previous item from an array, or loop to the end.
51
- def get_previous_item(list, index)
52
- decremented = index - 1
53
- if decremented < 0
54
- decremented = list.length() - 1
55
- end
56
- list[decremented]
57
- end
58
-
59
- # Get next item from an array, or loop to the beginning.
60
- def get_next_item(list, index)
61
- incremented = index + 1
62
- if incremented >= list.length()
63
- incremented = 0
64
- end
65
- list[incremented]
66
- end
67
-
68
- # Wrapper of get_previous_item specifically for indicators.
69
- def get_previous_indicator(list, index)
70
- indicator = get_previous_item(list, index)
71
- # Skip placeholder indicators.
72
- is_placeholder = (indicator.has_key?('placeholder') and indicator['placeholder'] != '')
73
- while (is_placeholder)
74
- index -= 1
75
- if index < 0
76
- index = list.length() - 1
77
- end
78
- indicator = get_previous_item(list, index)
79
- is_placeholder = (indicator.has_key?('placeholder') and indicator['placeholder'] != '')
80
- end
81
- return indicator
82
- end
83
-
84
- # Wrapper of get_next_item specifically for indicators.
85
- def get_next_indicator(list, index)
86
- indicator = get_next_item(list, index)
87
- # Skip placeholder indicators.
88
- is_placeholder = (indicator.has_key?('placeholder') and indicator['placeholder'] != '')
89
- while (is_placeholder)
90
- index += 1
91
- if index >= list.length()
92
- index = 0
93
- end
94
- indicator = get_next_item(list, index)
95
- is_placeholder = (indicator.has_key?('placeholder') and indicator['placeholder'] != '')
96
- end
97
- return indicator
98
- end
99
-
100
- # The Jekyll baseurl is user-configured, and can be inconsistent. This
101
- # ensure it is consistent in whether it starts/ends with a slash.
102
- def normalize_baseurl(baseurl)
103
- if baseurl == '' || baseurl.nil?
104
- baseurl = '/'
105
- end
106
- if !baseurl.start_with? '/'
107
- baseurl = '/' + baseurl
108
- end
109
- if !baseurl.end_with? '/'
110
- baseurl = baseurl + '/'
111
- end
112
- baseurl
113
- end
114
-
115
- # Compute a URL for an item, given it's number.
116
- def get_url(baseurl, language, number, languages, languages_public)
117
-
118
- baseurl = normalize_baseurl(baseurl)
119
-
120
- default_language = languages[0]
121
- language_public = language
122
- if languages_public && languages_public[language]
123
- language_public = languages_public[language]
124
- end
125
- if default_language != language
126
- baseurl += language_public + '/'
127
- end
128
-
129
- number = number.gsub('.', '-')
130
- baseurl + number
131
- end
132
-
133
- # Get a Hash of all the URLs based on one particular one.
134
- def get_all_urls(url, language, languages, languages_public, baseurl)
135
-
136
- baseurl = normalize_baseurl(baseurl)
137
-
138
- language_public = language
139
- if languages_public && languages_public[language]
140
- language_public = languages_public[language]
141
- end
142
-
143
- # First figure out the language-free URL.
144
- default_language = languages[0]
145
- if language == default_language
146
- url_without_language = url
147
- else
148
- url_without_language = url.gsub('/' + language_public + '/', '/')
149
- end
150
-
151
- urls = {
152
- language => url
153
- }
154
- if language != default_language
155
- default_language_url = baseurl + url_without_language
156
- # Fix potential double-slash.
157
- default_language_url = default_language_url.gsub('//', '/')
158
- urls[default_language] = default_language_url
159
- end
160
- languages.each do |other_language|
161
- if other_language == language
162
- next
163
- end
164
- if other_language == default_language
165
- next
166
- end
167
- other_language_public = other_language
168
- if languages_public && languages_public[other_language]
169
- other_language_public = languages_public[other_language]
170
- end
171
- urls[other_language] = baseurl + other_language_public + url_without_language
172
- end
173
- urls
174
- end
175
-
176
- # Compute a URL for tha goal image, given it's number.
177
- def get_goal_image(goal_image_base, language, number, extension)
178
- goal_image_base + '/' + language + '/' + number + '.' + extension
179
- end
180
-
181
- # This creates variables for use in Liquid templates under "page".
182
- # We'll create lists of goals, targets, and indicators. These will be put
183
- # on the page object. Eg: page.goals. In order to generate these lists
184
- # we will make use of the metadata. Each item in the list will be a hash
185
- # containing these keys:
186
- # - name (translated)
187
- # - number (the "id" or number, eg: 1, 1.2, 1.2.1, etc.)
188
- # - slug (version of 'number' but with dashes instead of dots)
189
- # - sort (for the purposes of sorting the items, if needed)
190
- # - global (a Hash containing any equivalent global metadata)
191
- # The goal hashes contain additional keys:
192
- # - short (the translated short version of the name)
193
- # - icon (path to the translated icon)
194
- # - url (path to the goal page)
195
- # The target hashes contain additional keys:
196
- # - goal_number (the goal number for this target)
197
- # The indicator hashes contain additional keys:
198
- # - url (path to the indicator page)
199
- # - goal_number (the goal number for this indicator)
200
- # - target_number (the target number for this indicator)
201
- # - [all metadata fields from the indicator]
202
- # The lists are:
203
- # - goals
204
- # - targets
205
- # - indicators
206
- # Additionally, on indicator pages themselves, there are variables for
207
- # the current goal/target/indicator:
208
- # - goal
209
- # - target
210
- # - indicator
211
- # Similarly, on goal pages themselves, there are variables for the current
212
- # goal:
213
- # - goal
214
- def generate(site)
215
-
216
- # Some general variables needed below.
217
- translations = site.data['translations']
218
- languages = site.config['languages']
219
- languages_public = opensdg_languages_public(site)
220
- default_language = languages[0]
221
- baseurl = site.config['baseurl']
222
- goal_image_base = 'https://open-sdg.org/sdg-translations/assets/img/goals'
223
- if site.config.has_key? 'goal_image_base'
224
- goal_image_base = site.config['goal_image_base']
225
- end
226
- goal_image_extension = 'png'
227
- if site.config.has_key?('goal_image_extension') && site.config['goal_image_extension'] != ''
228
- goal_image_extension = site.config['goal_image_extension']
229
- end
230
-
231
- # These keys are flagged as "protected" here so that we can make sure that
232
- # country-specific metadata doesn't use any of these fields.
233
- protected_keys = ['goals', 'goal', 'targets', 'target', 'indicators',
234
- 'indicator', 'language', 'name', 'number', 'sort', 'global', 'url',
235
- 'goal_number', 'target_number'
236
- ]
237
-
238
- # Figure out from our translations the global indicator numbers.
239
- global_inids = translations[default_language]['global_indicators'].keys
240
- global_inids = global_inids.select { |x| x.end_with? '-title' }
241
- global_inids = global_inids.map { |x| x.gsub('-title', '').gsub('-', '.') }
242
-
243
- # For available indicators, we simply map the "indicators" collection.
244
- available_inids = site.collections['indicators'].docs.select { |x| x.data['language'] == default_language }
245
- available_inids = available_inids.map { |x| x.data['indicator'] }
246
- available_indicators = {}
247
- available_targets = {}
248
- available_goals = {}
249
-
250
- # Some throwaway variables to keep track of what has been added.
251
- already_added = {}
252
-
253
- # Set up some empty hashes, per language.
254
- languages.each do |language|
255
- available_goals[language] = []
256
- available_targets[language] = []
257
- available_indicators[language] = []
258
- already_added[language] = []
259
- end
260
-
261
- # Populate the hashes.
262
- available_inids.each do |indicator_number|
263
- goal_number = get_goal_number(indicator_number)
264
- target_number = get_target_number(indicator_number)
265
- is_global_indicator = global_inids.index(indicator_number) != nil
266
-
267
- # To get the name of global stuff, we can use predicable translation
268
- # keys from the SDG Translations project. Eg: global_goals.1-title
269
- goal_translation_key = 'global_goals.' + goal_number
270
- target_translation_key = 'global_targets.' + target_number.gsub('.', '-')
271
- indicator_translation_key = 'global_indicators.' + indicator_number.gsub('.', '-')
272
-
273
- languages.each do |language|
274
- global_goal = {
275
- 'name' => opensdg_translate_key(goal_translation_key + '-title', translations, language),
276
- # TODO: More global metadata about goals?
277
- }
278
- global_target = {
279
- 'name' => opensdg_translate_key(target_translation_key + '-title', translations, language),
280
- # TODO: More global metadata about targets?
281
- }
282
- global_indicator = {}
283
- if is_global_indicator
284
- global_indicator = {
285
- 'name' => opensdg_translate_key(indicator_translation_key + '-title', translations, language),
286
- # TODO: More global metadata about indicators?
287
- }
288
- end
289
-
290
- # We have to get the metadata for the indicator/language.
291
- meta = {}
292
- # Currently the meta keys are dash-delimited. This is a little
293
- # arbitrary (it's because they came from filenames) and could maybe
294
- # be changed eventually to dot-delimited for consistency.
295
- meta_key = indicator_number.gsub('.', '-')
296
- # The location of the metadata is different depending on whether we are
297
- # using "translated_builds" or not.
298
- if opensdg_translated_builds(site)
299
- meta = site.data[language]['meta'][meta_key]
300
- else
301
- meta = site.data['meta'][meta_key]
302
- # Also for untranslated builds, we need to support the "subfolder"
303
- # approach for metadata translation. (This is handled at build-time
304
- # for translated builds.)
305
- if meta.has_key? language
306
- meta = meta.merge(meta[language])
307
- meta.delete(language)
308
- end
309
- end
310
-
311
- is_standalone = (meta.has_key?('standalone') and meta['standalone'])
312
- is_placeholder = (meta.has_key?('placeholder') and meta['placeholder'] != '')
313
-
314
- # Set the goal for this language, once only.
315
- if !is_standalone && already_added[language].index(goal_number) == nil
316
- already_added[language].push(goal_number)
317
- available_goal = {
318
- 'number' => goal_number,
319
- 'slug' => goal_number.gsub('.', '-'),
320
- 'name' => opensdg_translate_key(goal_translation_key + '-title', translations, language),
321
- 'short' => opensdg_translate_key(goal_translation_key + '-short', translations, language),
322
- 'url' => get_url(baseurl, language, goal_number, languages, languages_public),
323
- 'icon' => get_goal_image(goal_image_base, language, goal_number, goal_image_extension),
324
- 'sort' => get_sort_order(goal_number),
325
- 'global' => global_goal,
326
- }
327
- available_goals[language].push(available_goal)
328
- end
329
- # Set the target for this language, once only.
330
- if !is_standalone && already_added[language].index(target_number) == nil
331
- already_added[language].push(target_number)
332
- available_target = {
333
- 'number' => target_number,
334
- 'slug' => target_number.gsub('.', '-'),
335
- 'name' => opensdg_translate_key(target_translation_key + '-title', translations, language),
336
- 'sort' => get_sort_order(target_number),
337
- 'goal_number' => goal_number,
338
- 'global' => global_target,
339
- }
340
- available_targets[language].push(available_target)
341
- end
342
- # Set the indicator for this language. Unfortunately we are currently
343
- # using two possible fields for the indicator name:
344
- # - indicator_name
345
- # - indicator_name_national
346
- # TODO: Eventually standardize around 'indicator_name' and drop support
347
- # for 'indicator_name_national'.
348
- indicator_name = ''
349
- if meta.has_key? 'indicator_name_national'
350
- indicator_name = meta['indicator_name_national']
351
- else
352
- indicator_name = meta['indicator_name']
353
- end
354
- indicator_path = indicator_number
355
- if is_standalone && meta.has_key?('permalink') && meta['permalink'] != ''
356
- indicator_path = meta['permalink']
357
- end
358
- indicator_sort = get_sort_order(indicator_number)
359
- if meta.has_key?('sort') && meta['sort'] != ''
360
- # Allow metadata 'sort' field to override the default sort.
361
- indicator_sort = meta['sort']
362
- end
363
- if meta.has_key?('graph_annotations') && meta['graph_annotations'].length > 0
364
- meta['graph_annotations'].each do |annotation|
365
- if annotation.has_key?('borderDash') && annotation['borderDash'].is_a?(String)
366
- annotation['borderDash'] = [2, 2]
367
- opensdg_notice('The "borderDash" property in graph annotations must be an array. Using [2, 2].')
368
- end
369
- end
370
- end
371
- available_indicator = {
372
- 'number' => indicator_number,
373
- 'slug' => indicator_number.gsub('.', '-'),
374
- 'name' => opensdg_translate_key(indicator_name, translations, language),
375
- 'url' => get_url(baseurl, language, indicator_path, languages, languages_public),
376
- 'sort' => indicator_sort,
377
- 'goal_number' => goal_number,
378
- 'target_number' => target_number,
379
- 'global' => global_indicator,
380
- }
381
- # Translate and add any metadata.
382
- meta.each do |key, value|
383
- if !protected_keys.include? key
384
- available_indicator[key] = opensdg_translate_key(value, translations, language)
385
- end
386
- end
387
- available_indicators[language].push(available_indicator)
388
- end
389
- end
390
-
391
- # Sort all the items.
392
- languages.each do |lang|
393
- available_goals[lang] = available_goals[lang].sort_by { |x| x['sort'] }
394
- available_targets[lang] = available_targets[lang].sort_by { |x| x['sort'] }
395
- available_indicators[lang] = available_indicators[lang].sort_by { |x| x['sort'] }
396
- end
397
-
398
- # Next set the stuff on each doc in certain collections, according
399
- # to the doc's language. We'll be putting the global stuff on every
400
- # page, goal, and indicator across the site. This may be a bit memory-
401
- # intensive during the Jekyll build, but it is nice to have it available
402
- # for consistency.
403
- site.collections.keys.each do |collection|
404
- site.collections[collection].docs.each do |doc|
405
- # Ensure it has a language.
406
- if !doc.data.has_key? 'language'
407
- doc.data['language'] = default_language
408
- end
409
- # Ensure it has a valid language.
410
- if !languages.include? doc.data['language']
411
- message = "NOTICE: The document '#{doc.basename}' has an unexpected language '#{doc.data['language']}' so we are using the default language '#{default_language}' instead."
412
- opensdg_notice(message)
413
- doc.data['language'] = default_language
414
- end
415
- language = doc.data['language']
416
- # Set these on the page object.
417
- doc.data['goals'] = available_goals[language]
418
- doc.data['targets'] = available_targets[language]
419
- doc.data['indicators'] = available_indicators[language]
420
- doc.data['baseurl'] = get_url(baseurl, language, '', languages, languages_public)
421
- doc.data['url_by_language'] = get_all_urls(doc.url, language, languages, languages_public, baseurl)
422
- doc.data['t'] = site.data['translations'][language]
423
-
424
- # Set the remote_data_prefix for this page.
425
- if site.config.has_key?('remote_data_prefix') && opensdg_is_path_remote(site.config['remote_data_prefix'])
426
- doc.data['remote_data_prefix'] = site.config['remote_data_prefix']
427
- else
428
- doc.data['remote_data_prefix'] = normalize_baseurl(baseurl)
429
- end
430
- if opensdg_translated_builds(site)
431
- doc.data['remote_data_prefix_untranslated'] = File.join(doc.data['remote_data_prefix'], 'untranslated')
432
- doc.data['remote_data_prefix'] = File.join(doc.data['remote_data_prefix'], language)
433
- else
434
- doc.data['remote_data_prefix_untranslated'] = doc.data['remote_data_prefix']
435
- end
436
-
437
- # Set the logo for this page.
438
- logo = {}
439
- match = false
440
- if site.config.has_key?('logos') && site.config['logos'].length > 0
441
- match = site.config['logos'].find{ |item| item['language'] == language }
442
- unless match
443
- match = site.config['logos'].find{ |item| item.fetch('language', '') == '' }
444
- end
445
- end
446
- if match
447
- src = match['src']
448
- unless src.start_with?('http')
449
- src = normalize_baseurl(baseurl) + src
450
- end
451
- logo['src'] = src
452
- logo['alt'] = opensdg_translate_key(match['alt'], translations, language)
453
- else
454
- logo['src'] = normalize_baseurl(baseurl) + 'assets/img/SDG_logo.png'
455
- alt_text = opensdg_translate_key('general.sdg', translations, language)
456
- alt_text += ' - '
457
- alt_text += opensdg_translate_key('header.tag_line', translations, language)
458
- logo['alt'] = alt_text
459
- end
460
- doc.data['logo'] = logo
461
-
462
- if collection == 'indicators'
463
- # For indicators we also set the current indicator/target/goal.
464
- if doc.data.has_key? 'indicator_number'
465
- indicator_number = doc.data['indicator_number']
466
- elsif doc.data.has_key? 'indicator'
467
- # Backwards compatibility.
468
- indicator_number = doc.data['indicator']
469
- else
470
- raise "Error: An indicator does not have 'indicator_number' property."
471
- end
472
- # Force the indicator number to be a string.
473
- if indicator_number.is_a? Numeric
474
- indicator_number = indicator_number.to_s
475
- end
476
- goal_number = get_goal_number(indicator_number)
477
- target_number = get_target_number(indicator_number)
478
- doc.data['goal'] = available_goals[language].find {|x| x['number'] == goal_number}
479
- doc.data['target'] = available_targets[language].find {|x| x['number'] == target_number}
480
- indicator_index = available_indicators[language].find_index {|x| x['number'] == indicator_number}
481
- doc.data['indicator'] = available_indicators[language][indicator_index]
482
- doc.data['next'] = get_next_indicator(available_indicators[language], indicator_index)
483
- doc.data['previous'] = get_previous_indicator(available_indicators[language], indicator_index)
484
-
485
- elsif collection == 'goals'
486
- # For goals we also set the current goal.
487
- if doc.data.has_key? 'goal_number'
488
- goal_number = doc.data['goal_number']
489
- elsif doc.data.has_key? 'sdg_goal'
490
- # Backwards compatibility.
491
- goal_number = doc.data['sdg_goal']
492
- else
493
- raise "Error: A goal does not have 'goal_number' property."
494
- end
495
- # Force the goal number to be a string.
496
- if goal_number.is_a? Numeric
497
- goal_number = goal_number.to_s
498
- end
499
- goal_index = available_goals[language].find_index {|x| x['number'] == goal_number}
500
- doc.data['goal'] = available_goals[language][goal_index]
501
- doc.data['next'] = get_next_item(available_goals[language], goal_index)
502
- doc.data['previous'] = get_previous_item(available_goals[language], goal_index)
503
- end
504
- end
505
- end
506
-
507
- # Finally let's set all these on the site object so that they can be
508
- # easily looked up later.
509
- lookup = {}
510
- available_goals.each do |language, items|
511
- lookup[language] = {}
512
- items.each do |item|
513
- number = item['number']
514
- lookup[language][number] = item
515
- end
516
- end
517
- available_targets.each do |language, items|
518
- items.each do |item|
519
- number = item['number']
520
- lookup[language][number] = item
521
- end
522
- end
523
- available_indicators.each do |language, items|
524
- items.each do |item|
525
- number = item['number']
526
- lookup[language][number] = item
527
- end
528
- end
529
- site.data['sdg_lookup'] = lookup
530
-
531
- end
532
- end
533
- end
534
-
535
- module Jekyll
536
- module SDGLookup
537
- # This provides a "sdg_lookup" filter that takes an id and returns a hash
538
- # representation of a goal, target, or indicator.
539
- def sdg_lookup(number)
540
- number = number.gsub('-', '.')
541
- data = @context.registers[:site].data
542
- page = @context.environments.first['page']
543
- language = page['language']
544
- return data['sdg_lookup'][language][number]
545
- end
546
- end
547
- end
548
-
549
- Liquid::Template.register_filter(Jekyll::SDGLookup)
1
+ require "jekyll"
2
+ require_relative "helpers"
3
+
4
+ module JekyllOpenSdgPlugins
5
+ class SDGVariables < Jekyll::Generator
6
+ safe true
7
+ priority :low
8
+
9
+ # Get a goal number from an indicator number.
10
+ def get_goal_number(indicator_number)
11
+ parts = indicator_number.split('.')
12
+ parts[0]
13
+ end
14
+
15
+ # Get a target number from an indicator number.
16
+ def get_target_number(indicator_number)
17
+ parts = indicator_number.split('.')
18
+ if parts.length() < 2
19
+ indicator_number
20
+ else
21
+ parts[0] + '.' + parts[1]
22
+ end
23
+ end
24
+
25
+ # Is this string numeric?
26
+ def is_number? string
27
+ true if Float(string) rescue false
28
+ end
29
+
30
+ # Make any goal/target/indicator number suitable for use in sorting.
31
+ def get_sort_order(number)
32
+ if number.is_a? Numeric
33
+ number = number.to_s
34
+ end
35
+ sort_order = ''
36
+ parts = number.split('.')
37
+ parts.each do |part|
38
+ if part.length == 1
39
+ if is_number?(part)
40
+ part = '0' + part
41
+ else
42
+ part = part + part
43
+ end
44
+ end
45
+ sort_order += part
46
+ end
47
+ sort_order
48
+ end
49
+
50
+ # Get previous item from an array, or loop to the end.
51
+ def get_previous_item(list, index)
52
+ decremented = index - 1
53
+ if decremented < 0
54
+ decremented = list.length() - 1
55
+ end
56
+ list[decremented]
57
+ end
58
+
59
+ # Get next item from an array, or loop to the beginning.
60
+ def get_next_item(list, index)
61
+ incremented = index + 1
62
+ if incremented >= list.length()
63
+ incremented = 0
64
+ end
65
+ list[incremented]
66
+ end
67
+
68
+ # Wrapper of get_previous_item specifically for indicators.
69
+ def get_previous_indicator(list, index)
70
+ indicator = get_previous_item(list, index)
71
+ # Skip placeholder indicators.
72
+ is_placeholder = (indicator.has_key?('placeholder') and indicator['placeholder'] != '')
73
+ while (is_placeholder)
74
+ index -= 1
75
+ if index < 0
76
+ index = list.length() - 1
77
+ end
78
+ indicator = get_previous_item(list, index)
79
+ is_placeholder = (indicator.has_key?('placeholder') and indicator['placeholder'] != '')
80
+ end
81
+ return indicator
82
+ end
83
+
84
+ # Wrapper of get_next_item specifically for indicators.
85
+ def get_next_indicator(list, index)
86
+ indicator = get_next_item(list, index)
87
+ # Skip placeholder indicators.
88
+ is_placeholder = (indicator.has_key?('placeholder') and indicator['placeholder'] != '')
89
+ while (is_placeholder)
90
+ index += 1
91
+ if index >= list.length()
92
+ index = 0
93
+ end
94
+ indicator = get_next_item(list, index)
95
+ is_placeholder = (indicator.has_key?('placeholder') and indicator['placeholder'] != '')
96
+ end
97
+ return indicator
98
+ end
99
+
100
+ # The Jekyll baseurl is user-configured, and can be inconsistent. This
101
+ # ensure it is consistent in whether it starts/ends with a slash.
102
+ def normalize_baseurl(baseurl)
103
+ if baseurl == '' || baseurl.nil?
104
+ baseurl = '/'
105
+ end
106
+ if !baseurl.start_with? '/'
107
+ baseurl = '/' + baseurl
108
+ end
109
+ if !baseurl.end_with? '/'
110
+ baseurl = baseurl + '/'
111
+ end
112
+ baseurl
113
+ end
114
+
115
+ # Compute a URL for an item, given it's number.
116
+ def get_url(baseurl, language, number, languages, languages_public)
117
+
118
+ baseurl = normalize_baseurl(baseurl)
119
+
120
+ default_language = languages[0]
121
+ language_public = language
122
+ if languages_public && languages_public[language]
123
+ language_public = languages_public[language]
124
+ end
125
+ if default_language != language
126
+ baseurl += language_public + '/'
127
+ end
128
+
129
+ number = number.gsub('.', '-')
130
+ baseurl + number
131
+ end
132
+
133
+ # Get a Hash of all the URLs based on one particular one.
134
+ def get_all_urls(url, language, languages, languages_public, baseurl)
135
+
136
+ baseurl = normalize_baseurl(baseurl)
137
+
138
+ language_public = language
139
+ if languages_public && languages_public[language]
140
+ language_public = languages_public[language]
141
+ end
142
+
143
+ # First figure out the language-free URL.
144
+ default_language = languages[0]
145
+ if language == default_language
146
+ url_without_language = url
147
+ else
148
+ url_without_language = url.gsub('/' + language_public + '/', '/')
149
+ end
150
+
151
+ urls = {
152
+ language => url
153
+ }
154
+ if language != default_language
155
+ default_language_url = baseurl + url_without_language
156
+ # Fix potential double-slash.
157
+ default_language_url = default_language_url.gsub('//', '/')
158
+ urls[default_language] = default_language_url
159
+ end
160
+ languages.each do |other_language|
161
+ if other_language == language
162
+ next
163
+ end
164
+ if other_language == default_language
165
+ next
166
+ end
167
+ other_language_public = other_language
168
+ if languages_public && languages_public[other_language]
169
+ other_language_public = languages_public[other_language]
170
+ end
171
+ urls[other_language] = baseurl + other_language_public + url_without_language
172
+ end
173
+ urls
174
+ end
175
+
176
+ # Compute a URL for tha goal image, given it's number.
177
+ def get_goal_image(goal_image_base, language, number, extension)
178
+ goal_image_base + '/' + language + '/' + number + '.' + extension
179
+ end
180
+
181
+ # Calculate the correct content for the indicator tabs.
182
+ def set_indicator_tab_content(indicator_config, site_config)
183
+ defaults = {
184
+ 'tab_1' => 'chart',
185
+ 'tab_2' => 'table',
186
+ 'tab_3' => 'map',
187
+ 'tab_4' => 'embed',
188
+ }
189
+ # Use the site config or defaults if necessary.
190
+ tabs = site_config.has_key?('indicator_tabs') ? site_config['indicator_tabs'] : defaults
191
+ no_config = tabs.values.all? { |value| value == '' }
192
+ if no_config
193
+ tabs = defaults
194
+ end
195
+ # Override for this indicator if needed.
196
+ if indicator_config.has_key?('indicator_tabs')
197
+ if indicator_config['indicator_tabs'].has_key?('override')
198
+ if indicator_config['indicator_tabs']['override']
199
+ tabs = indicator_config['indicator_tabs']
200
+ end
201
+ end
202
+ end
203
+
204
+ embed_has_label = (indicator_config.has_key?('embedded_feature_tab_title') && indicator_config['embedded_feature_tab_title'] != '')
205
+ embed_label = embed_has_label ? indicator_config['embedded_feature_tab_title'] : 'Embed'
206
+
207
+ labels = {
208
+ 'chart' => 'indicator.chart',
209
+ 'table' => 'indicator.table',
210
+ 'map' => 'indicator.map',
211
+ 'embed' => embed_label,
212
+ }
213
+
214
+ tabs_list = []
215
+ ['tab_1', 'tab_2', 'tab_3', 'tab_4'].each do |tab_number|
216
+ type = tabs[tab_number]
217
+ if type == 'hide'
218
+ next
219
+ end
220
+
221
+ if type == 'embed'
222
+ embed_url = (indicator_config.has_key?('embedded_feature_url') && indicator_config['embedded_feature_url'] != '')
223
+ embed_html = (indicator_config.has_key?('embedded_feature_html') && indicator_config['embedded_feature_html'] != '')
224
+ unless embed_url || embed_html
225
+ next
226
+ end
227
+ elsif type == 'map'
228
+ show_map = (indicator_config.has_key?('data_show_map') && indicator_config['data_show_map'])
229
+ unless show_map
230
+ next
231
+ end
232
+ end
233
+
234
+ tabs_list.push({
235
+ 'type' => type,
236
+ 'label' => labels[type],
237
+ })
238
+ end
239
+
240
+ indicator_config['indicator_tabs_list'] = tabs_list
241
+ end
242
+
243
+ # This creates variables for use in Liquid templates under "page".
244
+ # We'll create lists of goals, targets, and indicators. These will be put
245
+ # on the page object. Eg: page.goals. In order to generate these lists
246
+ # we will make use of the metadata. Each item in the list will be a hash
247
+ # containing these keys:
248
+ # - name (translated)
249
+ # - number (the "id" or number, eg: 1, 1.2, 1.2.1, etc.)
250
+ # - slug (version of 'number' but with dashes instead of dots)
251
+ # - sort (for the purposes of sorting the items, if needed)
252
+ # - global (a Hash containing any equivalent global metadata)
253
+ # The goal hashes contain additional keys:
254
+ # - short (the translated short version of the name)
255
+ # - icon (path to the translated icon)
256
+ # - url (path to the goal page)
257
+ # The target hashes contain additional keys:
258
+ # - goal_number (the goal number for this target)
259
+ # The indicator hashes contain additional keys:
260
+ # - url (path to the indicator page)
261
+ # - goal_number (the goal number for this indicator)
262
+ # - target_number (the target number for this indicator)
263
+ # - [all metadata fields from the indicator]
264
+ # The lists are:
265
+ # - goals
266
+ # - targets
267
+ # - indicators
268
+ # Additionally, on indicator pages themselves, there are variables for
269
+ # the current goal/target/indicator:
270
+ # - goal
271
+ # - target
272
+ # - indicator
273
+ # Similarly, on goal pages themselves, there are variables for the current
274
+ # goal:
275
+ # - goal
276
+ def generate(site)
277
+
278
+ # Some general variables needed below.
279
+ translations = site.data['translations']
280
+ languages = site.config['languages']
281
+ languages_public = opensdg_languages_public(site)
282
+ default_language = languages[0]
283
+ baseurl = site.config['baseurl']
284
+ goal_image_base = 'https://open-sdg.org/sdg-translations/assets/img/goals'
285
+ if site.config.has_key? 'goal_image_base'
286
+ goal_image_base = site.config['goal_image_base']
287
+ end
288
+ goal_image_extension = 'png'
289
+ if site.config.has_key?('goal_image_extension') && site.config['goal_image_extension'] != ''
290
+ goal_image_extension = site.config['goal_image_extension']
291
+ end
292
+
293
+ # These keys are flagged as "protected" here so that we can make sure that
294
+ # country-specific metadata doesn't use any of these fields.
295
+ protected_keys = ['goals', 'goal', 'targets', 'target', 'indicators',
296
+ 'indicator', 'language', 'name', 'number', 'sort', 'global', 'url',
297
+ 'goal_number', 'target_number'
298
+ ]
299
+
300
+ # Figure out from our translations the global indicator numbers.
301
+ global_inids = translations[default_language]['global_indicators'].keys
302
+ global_inids = global_inids.select { |x| x.end_with? '-title' }
303
+ global_inids = global_inids.map { |x| x.gsub('-title', '').gsub('-', '.') }
304
+
305
+ # For available indicators, we simply map the "indicators" collection.
306
+ available_inids = site.collections['indicators'].docs.select { |x| x.data['language'] == default_language }
307
+ available_inids = available_inids.map { |x| x.data['indicator'] }
308
+ available_indicators = {}
309
+ available_targets = {}
310
+ available_goals = {}
311
+
312
+ # Some throwaway variables to keep track of what has been added.
313
+ already_added = {}
314
+
315
+ # Set up some empty hashes, per language.
316
+ languages.each do |language|
317
+ available_goals[language] = []
318
+ available_targets[language] = []
319
+ available_indicators[language] = []
320
+ already_added[language] = []
321
+ end
322
+
323
+ # Populate the hashes.
324
+ available_inids.each do |indicator_number|
325
+ goal_number = get_goal_number(indicator_number)
326
+ target_number = get_target_number(indicator_number)
327
+ is_global_indicator = global_inids.index(indicator_number) != nil
328
+
329
+ # To get the name of global stuff, we can use predicable translation
330
+ # keys from the SDG Translations project. Eg: global_goals.1-title
331
+ goal_translation_key = 'global_goals.' + goal_number
332
+ target_translation_key = 'global_targets.' + target_number.gsub('.', '-')
333
+ indicator_translation_key = 'global_indicators.' + indicator_number.gsub('.', '-')
334
+
335
+ languages.each do |language|
336
+ global_goal = {
337
+ 'name' => opensdg_translate_key(goal_translation_key + '-title', translations, language),
338
+ # TODO: More global metadata about goals?
339
+ }
340
+ global_target = {
341
+ 'name' => opensdg_translate_key(target_translation_key + '-title', translations, language),
342
+ # TODO: More global metadata about targets?
343
+ }
344
+ global_indicator = {}
345
+ if is_global_indicator
346
+ global_indicator = {
347
+ 'name' => opensdg_translate_key(indicator_translation_key + '-title', translations, language),
348
+ # TODO: More global metadata about indicators?
349
+ }
350
+ end
351
+
352
+ # We have to get the metadata for the indicator/language.
353
+ meta = {}
354
+ # Currently the meta keys are dash-delimited. This is a little
355
+ # arbitrary (it's because they came from filenames) and could maybe
356
+ # be changed eventually to dot-delimited for consistency.
357
+ meta_key = indicator_number.gsub('.', '-')
358
+ # The location of the metadata is different depending on whether we are
359
+ # using "translated_builds" or not.
360
+ if opensdg_translated_builds(site)
361
+ meta = site.data[language]['meta'][meta_key]
362
+ else
363
+ meta = site.data['meta'][meta_key]
364
+ # Also for untranslated builds, we need to support the "subfolder"
365
+ # approach for metadata translation. (This is handled at build-time
366
+ # for translated builds.)
367
+ if meta.has_key? language
368
+ meta = meta.merge(meta[language])
369
+ meta.delete(language)
370
+ end
371
+ end
372
+
373
+ is_standalone = (meta.has_key?('standalone') and meta['standalone'])
374
+ is_placeholder = (meta.has_key?('placeholder') and meta['placeholder'] != '')
375
+
376
+ # Set the goal for this language, once only.
377
+ if !is_standalone && already_added[language].index(goal_number) == nil
378
+ already_added[language].push(goal_number)
379
+ available_goal = {
380
+ 'number' => goal_number,
381
+ 'slug' => goal_number.gsub('.', '-'),
382
+ 'name' => opensdg_translate_key(goal_translation_key + '-title', translations, language),
383
+ 'short' => opensdg_translate_key(goal_translation_key + '-short', translations, language),
384
+ 'url' => get_url(baseurl, language, goal_number, languages, languages_public),
385
+ 'icon' => get_goal_image(goal_image_base, language, goal_number, goal_image_extension),
386
+ 'sort' => get_sort_order(goal_number),
387
+ 'global' => global_goal,
388
+ }
389
+ available_goals[language].push(available_goal)
390
+ end
391
+ # Set the target for this language, once only.
392
+ if !is_standalone && already_added[language].index(target_number) == nil
393
+ already_added[language].push(target_number)
394
+ available_target = {
395
+ 'number' => target_number,
396
+ 'slug' => target_number.gsub('.', '-'),
397
+ 'name' => opensdg_translate_key(target_translation_key + '-title', translations, language),
398
+ 'sort' => get_sort_order(target_number),
399
+ 'goal_number' => goal_number,
400
+ 'global' => global_target,
401
+ }
402
+ available_targets[language].push(available_target)
403
+ end
404
+ # Set the indicator for this language. Unfortunately we are currently
405
+ # using two possible fields for the indicator name:
406
+ # - indicator_name
407
+ # - indicator_name_national
408
+ # TODO: Eventually standardize around 'indicator_name' and drop support
409
+ # for 'indicator_name_national'.
410
+ indicator_name = ''
411
+ if meta.has_key? 'indicator_name_national'
412
+ indicator_name = meta['indicator_name_national']
413
+ else
414
+ indicator_name = meta['indicator_name']
415
+ end
416
+ indicator_path = indicator_number
417
+ if is_standalone && meta.has_key?('permalink') && meta['permalink'] != ''
418
+ indicator_path = meta['permalink']
419
+ end
420
+ indicator_sort = get_sort_order(indicator_number)
421
+ if meta.has_key?('sort') && meta['sort'] != ''
422
+ # Allow metadata 'sort' field to override the default sort.
423
+ indicator_sort = meta['sort']
424
+ end
425
+ if meta.has_key?('graph_annotations') && meta['graph_annotations'].length > 0
426
+ meta['graph_annotations'].each do |annotation|
427
+ if annotation.has_key?('borderDash') && annotation['borderDash'].is_a?(String)
428
+ annotation['borderDash'] = [2, 2]
429
+ opensdg_notice('The "borderDash" property in graph annotations must be an array. Using [2, 2].')
430
+ end
431
+ end
432
+ end
433
+ available_indicator = {
434
+ 'number' => indicator_number,
435
+ 'slug' => indicator_number.gsub('.', '-'),
436
+ 'name' => opensdg_translate_key(indicator_name, translations, language),
437
+ 'url' => get_url(baseurl, language, indicator_path, languages, languages_public),
438
+ 'sort' => indicator_sort,
439
+ 'goal_number' => goal_number,
440
+ 'target_number' => target_number,
441
+ 'global' => global_indicator,
442
+ }
443
+ # Translate and add any metadata.
444
+ meta.each do |key, value|
445
+ if !protected_keys.include? key
446
+ available_indicator[key] = opensdg_translate_key(value, translations, language)
447
+ end
448
+ end
449
+ available_indicators[language].push(available_indicator)
450
+ end
451
+ end
452
+
453
+ # Sort all the items.
454
+ languages.each do |lang|
455
+ available_goals[lang] = available_goals[lang].sort_by { |x| x['sort'] }
456
+ available_targets[lang] = available_targets[lang].sort_by { |x| x['sort'] }
457
+ available_indicators[lang] = available_indicators[lang].sort_by { |x| x['sort'] }
458
+ end
459
+
460
+ # Next set the stuff on each doc in certain collections, according
461
+ # to the doc's language. We'll be putting the global stuff on every
462
+ # page, goal, and indicator across the site. This may be a bit memory-
463
+ # intensive during the Jekyll build, but it is nice to have it available
464
+ # for consistency.
465
+ site.collections.keys.each do |collection|
466
+ site.collections[collection].docs.each do |doc|
467
+ # Ensure it has a language.
468
+ if !doc.data.has_key? 'language'
469
+ doc.data['language'] = default_language
470
+ end
471
+ # Ensure it has a valid language.
472
+ if !languages.include? doc.data['language']
473
+ message = "NOTICE: The document '#{doc.basename}' has an unexpected language '#{doc.data['language']}' so we are using the default language '#{default_language}' instead."
474
+ opensdg_notice(message)
475
+ doc.data['language'] = default_language
476
+ end
477
+ language = doc.data['language']
478
+ # Set these on the page object.
479
+ doc.data['goals'] = available_goals[language]
480
+ doc.data['targets'] = available_targets[language]
481
+ doc.data['indicators'] = available_indicators[language]
482
+ doc.data['baseurl'] = get_url(baseurl, language, '', languages, languages_public)
483
+ doc.data['url_by_language'] = get_all_urls(doc.url, language, languages, languages_public, baseurl)
484
+ doc.data['t'] = site.data['translations'][language]
485
+
486
+ # Set the remote_data_prefix for this page.
487
+ if site.config.has_key?('remote_data_prefix') && opensdg_is_path_remote(site.config['remote_data_prefix'])
488
+ doc.data['remote_data_prefix'] = site.config['remote_data_prefix']
489
+ else
490
+ doc.data['remote_data_prefix'] = normalize_baseurl(baseurl)
491
+ end
492
+ if opensdg_translated_builds(site)
493
+ doc.data['remote_data_prefix_untranslated'] = File.join(doc.data['remote_data_prefix'], 'untranslated')
494
+ doc.data['remote_data_prefix'] = File.join(doc.data['remote_data_prefix'], language)
495
+ else
496
+ doc.data['remote_data_prefix_untranslated'] = doc.data['remote_data_prefix']
497
+ end
498
+
499
+ # Set the logo for this page.
500
+ logo = {}
501
+ match = false
502
+ if site.config.has_key?('logos') && site.config['logos'].length > 0
503
+ match = site.config['logos'].find{ |item| item['language'] == language }
504
+ unless match
505
+ match = site.config['logos'].find{ |item| item.fetch('language', '') == '' }
506
+ end
507
+ end
508
+ if match
509
+ src = match['src']
510
+ unless src.start_with?('http')
511
+ src = normalize_baseurl(baseurl) + src
512
+ end
513
+ logo['src'] = src
514
+ logo['alt'] = opensdg_translate_key(match['alt'], translations, language)
515
+ else
516
+ logo['src'] = normalize_baseurl(baseurl) + 'assets/img/SDG_logo.png'
517
+ alt_text = opensdg_translate_key('general.sdg', translations, language)
518
+ alt_text += ' - '
519
+ alt_text += opensdg_translate_key('header.tag_line', translations, language)
520
+ logo['alt'] = alt_text
521
+ end
522
+ doc.data['logo'] = logo
523
+
524
+ if collection == 'indicators'
525
+ # For indicators we also set the current indicator/target/goal.
526
+ if doc.data.has_key? 'indicator_number'
527
+ indicator_number = doc.data['indicator_number']
528
+ elsif doc.data.has_key? 'indicator'
529
+ # Backwards compatibility.
530
+ indicator_number = doc.data['indicator']
531
+ else
532
+ raise "Error: An indicator does not have 'indicator_number' property."
533
+ end
534
+ # Force the indicator number to be a string.
535
+ if indicator_number.is_a? Numeric
536
+ indicator_number = indicator_number.to_s
537
+ end
538
+ goal_number = get_goal_number(indicator_number)
539
+ target_number = get_target_number(indicator_number)
540
+ doc.data['goal'] = available_goals[language].find {|x| x['number'] == goal_number}
541
+ doc.data['target'] = available_targets[language].find {|x| x['number'] == target_number}
542
+ indicator_index = available_indicators[language].find_index {|x| x['number'] == indicator_number}
543
+ doc.data['indicator'] = available_indicators[language][indicator_index]
544
+ doc.data['next'] = get_next_indicator(available_indicators[language], indicator_index)
545
+ doc.data['previous'] = get_previous_indicator(available_indicators[language], indicator_index)
546
+
547
+ # Also calculate the content for the indicator tabs.
548
+ set_indicator_tab_content(doc.data['indicator'], site.config)
549
+
550
+ elsif collection == 'goals'
551
+ # For goals we also set the current goal.
552
+ if doc.data.has_key? 'goal_number'
553
+ goal_number = doc.data['goal_number']
554
+ elsif doc.data.has_key? 'sdg_goal'
555
+ # Backwards compatibility.
556
+ goal_number = doc.data['sdg_goal']
557
+ else
558
+ raise "Error: A goal does not have 'goal_number' property."
559
+ end
560
+ # Force the goal number to be a string.
561
+ if goal_number.is_a? Numeric
562
+ goal_number = goal_number.to_s
563
+ end
564
+ goal_index = available_goals[language].find_index {|x| x['number'] == goal_number}
565
+ doc.data['goal'] = available_goals[language][goal_index]
566
+ doc.data['next'] = get_next_item(available_goals[language], goal_index)
567
+ doc.data['previous'] = get_previous_item(available_goals[language], goal_index)
568
+ end
569
+ end
570
+ end
571
+
572
+ # Finally let's set all these on the site object so that they can be
573
+ # easily looked up later.
574
+ lookup = {}
575
+ available_goals.each do |language, items|
576
+ lookup[language] = {}
577
+ items.each do |item|
578
+ number = item['number']
579
+ lookup[language][number] = item
580
+ end
581
+ end
582
+ available_targets.each do |language, items|
583
+ items.each do |item|
584
+ number = item['number']
585
+ lookup[language][number] = item
586
+ end
587
+ end
588
+ available_indicators.each do |language, items|
589
+ items.each do |item|
590
+ number = item['number']
591
+ lookup[language][number] = item
592
+ end
593
+ end
594
+ site.data['sdg_lookup'] = lookup
595
+
596
+ end
597
+ end
598
+ end
599
+
600
+ module Jekyll
601
+ module SDGLookup
602
+ # This provides a "sdg_lookup" filter that takes an id and returns a hash
603
+ # representation of a goal, target, or indicator.
604
+ def sdg_lookup(number)
605
+ number = number.gsub('-', '.')
606
+ data = @context.registers[:site].data
607
+ page = @context.environments.first['page']
608
+ language = page['language']
609
+ return data['sdg_lookup'][language][number]
610
+ end
611
+ end
612
+ end
613
+
614
+ Liquid::Template.register_filter(Jekyll::SDGLookup)