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

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.
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 -64
  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 -1607
  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 -73
  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 +5 -5
@@ -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)