jekyll-geolexica 1.0.0

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 (75) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +6 -0
  3. data/README.adoc +102 -0
  4. data/_config.yml +88 -0
  5. data/_data/lang.yaml +96 -0
  6. data/_includes/_title.html +5 -0
  7. data/_includes/head.html +48 -0
  8. data/_includes/localized-concept.html +99 -0
  9. data/_includes/newsroll-entry.html +17 -0
  10. data/_includes/page-header.html +31 -0
  11. data/_includes/resource-tree-item.html +49 -0
  12. data/_includes/script.html +0 -0
  13. data/_layouts/base-page.html +7 -0
  14. data/_layouts/concept.html +154 -0
  15. data/_layouts/concept.jsonld.html +152 -0
  16. data/_layouts/concept.ttl.html +83 -0
  17. data/_layouts/custom-home.html +33 -0
  18. data/_layouts/custom-post.html +7 -0
  19. data/_layouts/default.html +176 -0
  20. data/_layouts/home.html +6 -0
  21. data/_layouts/page.html +6 -0
  22. data/_layouts/post.html +13 -0
  23. data/_layouts/posts.html +10 -0
  24. data/_layouts/resource-index.html +14 -0
  25. data/_layouts/resource-page.html +25 -0
  26. data/_pages/404.adoc +12 -0
  27. data/_pages/api/rdf-profile.ttl +225 -0
  28. data/_pages/concepts-index-list.json +24 -0
  29. data/_pages/concepts-index.json +14 -0
  30. data/_pages/concepts.adoc +38 -0
  31. data/_pages/index.adoc +8 -0
  32. data/_pages/posts.adoc +6 -0
  33. data/_pages/stats.adoc +18 -0
  34. data/_pages/stats.json +5 -0
  35. data/_sass/adoc-markup.scss +197 -0
  36. data/_sass/concept.scss +171 -0
  37. data/_sass/concepts.scss +18 -0
  38. data/_sass/expandable-nav.scss +187 -0
  39. data/_sass/geolexica_home.scss +174 -0
  40. data/_sass/home.scss +87 -0
  41. data/_sass/jekyll-theme-isotc211.scss +146 -0
  42. data/_sass/legacy-crossbrowser.scss +67 -0
  43. data/_sass/main.scss +413 -0
  44. data/_sass/mixins.scss +39 -0
  45. data/_sass/normalize.scss +424 -0
  46. data/_sass/offsets.scss +59 -0
  47. data/_sass/post.scss +16 -0
  48. data/_sass/posts.scss +18 -0
  49. data/assets/algolia-search.js +28 -0
  50. data/assets/js/concept-search-worker.js +103 -0
  51. data/assets/js/concept-search.js +293 -0
  52. data/assets/js/ga.js +15 -0
  53. data/assets/js/nav.js +125 -0
  54. data/assets/js/resource-browser.js +79 -0
  55. data/assets/logo-ribose.svg +1 -0
  56. data/assets/resource-viewer-placeholder.html +11 -0
  57. data/assets/style.scss +11 -0
  58. data/babel.config.js +16 -0
  59. data/browserconfig.xml +12 -0
  60. data/fonts/MetaWebPro-Normal.woff +0 -0
  61. data/fonts/MetaWebPro-Thin.woff +0 -0
  62. data/jekyll-geolexica.gemspec +45 -0
  63. data/lib/jekyll-geolexica.rb +5 -0
  64. data/lib/jekyll/geolexica.rb +19 -0
  65. data/lib/jekyll/geolexica/concept_page.rb +169 -0
  66. data/lib/jekyll/geolexica/concept_serializer.rb +44 -0
  67. data/lib/jekyll/geolexica/concepts_generator.rb +64 -0
  68. data/lib/jekyll/geolexica/configuration.rb +47 -0
  69. data/lib/jekyll/geolexica/glossary.rb +95 -0
  70. data/lib/jekyll/geolexica/hooks.rb +33 -0
  71. data/lib/jekyll/geolexica/meta_pages_generator.rb +58 -0
  72. data/lib/jekyll/geolexica/version.rb +8 -0
  73. data/package-lock.json +2921 -0
  74. data/package.json +10 -0
  75. metadata +209 -0
@@ -0,0 +1,59 @@
1
+ // Offsets
2
+ // =======
3
+
4
+ $sideOffsetBase: 15vw;
5
+
6
+ body > header {
7
+ padding: 0 $sideOffsetBase 0 $sideOffsetBase;
8
+
9
+ // Hanging logo on the left
10
+ @media screen and (min-width: $bigscreenBreakpoint) {
11
+ padding: 0 $sideOffsetBase 0 calc(#{$sideOffsetBase} - #{$logoOffset});
12
+ }
13
+ }
14
+ body > footer {
15
+ padding: 0 $sideOffsetBase 0 $sideOffsetBase;
16
+
17
+ // Hanging logo on the right
18
+ @media screen and (min-width: $bigscreenBreakpoint) {
19
+ padding: 0 calc(#{$sideOffsetBase} - #{$logoOffset}) 0 $sideOffsetBase;
20
+ }
21
+ }
22
+
23
+ body.home > main {
24
+ > section .section-title,
25
+ > .section > h2,
26
+ > .section > .sectionbody {
27
+ margin-left: $sideOffsetBase;
28
+ margin-right: $sideOffsetBase;
29
+ }
30
+ }
31
+
32
+ body.home > main > .news {
33
+ .items {
34
+ margin-left: calc(#{$sideOffsetBase} - #{$homeSectionItemSidePadding});
35
+ }
36
+ }
37
+
38
+ // Basic body
39
+ .pad-all-main-contents {
40
+ > main > * {
41
+ padding-left: $sideOffsetBase;
42
+ padding-right: $sideOffsetBase / 2;
43
+
44
+ @media screen and (min-width: $bigscreenBreakpoint) {
45
+ padding-right: $sideOffsetBase;
46
+ }
47
+ }
48
+ }
49
+
50
+ body.post, body.page, body.post-index, body.resource-index {
51
+ @extend .pad-all-main-contents;
52
+ }
53
+
54
+ body.resource {
55
+ > main > * {
56
+ padding-left: $stripeWidth * 2;
57
+ padding-right: 0;
58
+ }
59
+ }
data/_sass/post.scss ADDED
@@ -0,0 +1,16 @@
1
+ body.post,
2
+ body.page {
3
+ > main {
4
+ .illustration {
5
+ margin-bottom: 1em;
6
+
7
+ img {
8
+ display: block;
9
+ width: 100%;
10
+ @media screen and (min-width: $bigscreenBreakpoint) {
11
+ width: 85%;
12
+ }
13
+ }
14
+ }
15
+ }
16
+ }
data/_sass/posts.scss ADDED
@@ -0,0 +1,18 @@
1
+ body.post-index {
2
+ > main {
3
+ h1 {
4
+ font-weight: normal;
5
+ font-size: 200%;
6
+ }
7
+
8
+ .news-item-card {
9
+ h3 {
10
+ margin-bottom: 0;
11
+ }
12
+ .meta {
13
+ color: lighten($textColor, 50);
14
+ font-size: 80%;
15
+ }
16
+ }
17
+ }
18
+ }
@@ -0,0 +1,28 @@
1
+ ---
2
+ ---
3
+
4
+ {% if site.algolia %}
5
+ // Instanciating InstantSearch.js with Algolia credentials
6
+ var search = instantsearch({
7
+ appId: '{{ site.algolia.application_id }}',
8
+ indexName: '{{ site.algolia.index_name }}',
9
+ apiKey: '{{ site.algolia.search_only_api_key }}'
10
+ });
11
+
12
+ // Adding searchbar and results widgets
13
+ search.addWidget(
14
+ instantsearch.widgets.searchBox({
15
+ container: '#search-searchbar',
16
+ placeholder: 'Search into posts...',
17
+ poweredBy: true // This is required if you're on the free Community plan
18
+ })
19
+ );
20
+ search.addWidget(
21
+ instantsearch.widgets.hits({
22
+ container: '#search-hits'
23
+ })
24
+ );
25
+
26
+ // Starting the search
27
+ search.start();
28
+ {% endif %}
@@ -0,0 +1,103 @@
1
+ importScripts('/assets/js/babel-polyfill.js');
2
+
3
+ const CONCEPTS_URL = '/api/concepts-index-list.json';
4
+
5
+ const LANGUAGES = [
6
+ 'eng',
7
+ 'ara',
8
+ 'spa',
9
+ 'swe',
10
+ 'kor',
11
+ 'rus',
12
+ 'ger',
13
+ 'fre',
14
+ 'fin',
15
+ 'jpn',
16
+ 'dan',
17
+ 'chi',
18
+ ];
19
+
20
+ var concepts = null;
21
+ var latestQuery = null;
22
+
23
+ function fetchConcepts() {
24
+ if (concepts === null) {
25
+ concepts = fetch(CONCEPTS_URL).then((resp) => resp.json());
26
+ }
27
+ return concepts;
28
+ }
29
+
30
+ async function filterAndSort(params) {
31
+ var concepts = await fetchConcepts();
32
+
33
+ if (params.string !== '') {
34
+ concepts = concepts.map((_item) => {
35
+ // Search all localized term names for the presence of given search string
36
+
37
+ const item = Object.assign({}, _item);
38
+ const queryString = params.string.toLowerCase();
39
+ const matchingLanguages = LANGUAGES.
40
+ filter((lang) => {
41
+ const term = (item[lang] || {}).term;
42
+ return term && term.toLowerCase().indexOf(params.string) >= 0;
43
+ });
44
+
45
+ if (matchingLanguages.length > 0) {
46
+ for (let lang of LANGUAGES) {
47
+ if (matchingLanguages.indexOf(lang) < 0) {
48
+ delete item[lang];
49
+ }
50
+ }
51
+ return item;
52
+ } else {
53
+ return null;
54
+ }
55
+ }).filter((item) => item !== null);
56
+ }
57
+
58
+ if (params.valid !== undefined) {
59
+ concepts = concepts.
60
+ filter((item) => {
61
+ // Only select concepts with at least one localized version matching given validity query
62
+ const validLocalizedItems = LANGUAGES.
63
+ filter((lang) => item.hasOwnProperty(lang)).
64
+ filter((lang) => item[lang].entry_status === params.valid);
65
+ return validLocalizedItems.length > 0;
66
+ }).
67
+ map((_item) => {
68
+ // Delete localized versions that don’t match given validity query
69
+
70
+ const item = Object.assign({}, _item);
71
+ for (let lang of LANGUAGES) {
72
+ if (item[lang] && item[lang].entry_status !== params.valid) {
73
+ delete item[lang];
74
+ }
75
+ }
76
+ return item;
77
+ });
78
+ }
79
+
80
+ return concepts.sort((item1, item2) => item1.termid - item2.termid);
81
+ }
82
+
83
+ onmessage = async function(msg) {
84
+ latestQuery = msg.data;
85
+
86
+ let concepts;
87
+ try {
88
+ concepts = await filterAndSort(msg.data);
89
+ } catch (e) {
90
+ console.error(e);
91
+ postMessage({ error: "Failed to fetch concepts, please <a href='javascript:window.location.reload();'>reload</a> & try again!" });
92
+ throw e;
93
+ return;
94
+ }
95
+
96
+ // Check if we the query changed while concepts were being fetched,
97
+ // in that case skip posting back the message
98
+ // NOTE: if more query parameters are supported, update the condition to ensure
99
+ // full comparison
100
+ if (latestQuery.string === msg.data.string) {
101
+ postMessage(concepts);
102
+ }
103
+ };
@@ -0,0 +1,293 @@
1
+ (function () {
2
+
3
+ const searchWorker = new Worker('/assets/js/concept-search-worker.js');
4
+
5
+ // TODO: Move to a shared module
6
+ const LANGUAGES = [
7
+ 'eng',
8
+ 'ara',
9
+ 'spa',
10
+ 'swe',
11
+ 'kor',
12
+ 'rus',
13
+ 'ger',
14
+ 'fre',
15
+ 'fin',
16
+ 'jpn',
17
+ 'dan',
18
+ 'chi',
19
+ ];
20
+
21
+
22
+ // React-based concept browser
23
+ // ===========================
24
+
25
+ let el = React.createElement;
26
+
27
+ function maybeConceptLinkForField(fieldName) {
28
+ return (concept) => {
29
+ const link = getConceptPermalink(concept);
30
+ if (link) {
31
+ return el('a', { href: link, target: '_blank', }, concept[fieldName]);
32
+ } else {
33
+ return el('span', null, concept[fieldName]);
34
+ }
35
+ }
36
+ }
37
+
38
+ let fieldConfig = {
39
+ termid: { title: 'Term ID', view: maybeConceptLinkForField('termid'), },
40
+ term: { title: 'Term', view: maybeConceptLinkForField('term'), },
41
+ language_code: { title: 'Lang' },
42
+ entry_status: { title: 'Validity' },
43
+ review_decision: { title: 'Review' },
44
+ };
45
+
46
+ let fields = ['termid', 'language_code', 'term', 'entry_status', 'review_decision'].map((f) => {
47
+ return { name: f, ...fieldConfig[f] };
48
+ });
49
+
50
+ class SearchControls extends React.Component {
51
+ constructor(props) {
52
+ super();
53
+
54
+ this.handleSearchStringChange = this.handleSearchStringChange.bind(this);
55
+ this.handleValiditySelectionChange = this.handleValiditySelectionChange.bind(this);
56
+
57
+ this.stringInputRef = React.createRef();
58
+
59
+ this.state = {
60
+ valid: 'valid', // Required value of the entry_status field, or undefined
61
+ string: '',
62
+ };
63
+ }
64
+ componentDidMount() {
65
+ this.stringInputRef.current.focus();
66
+ }
67
+ render() {
68
+ var searchControls = [
69
+ el('input', {
70
+ key: 'search-string',
71
+ ref: this.stringInputRef,
72
+ className: 'search-string',
73
+ type: 'text',
74
+ placeholder: 'Start typing…',
75
+ onChange: this.handleSearchStringChange}),
76
+ ];
77
+
78
+ if (this.state.string.length > 1 && (this.props.refineControls || []).length > 0) {
79
+ var refineControls = [];
80
+
81
+ if (this.props.refineControls.indexOf('validity') >= 0) {
82
+ refineControls.push(
83
+ el('div', { key: 'validity', className: 'validity' }, [
84
+ el('input', {
85
+ key: 'validity-checkbox',
86
+ id: 'conceptSearchValidity',
87
+ type: 'checkbox',
88
+ checked: this.state.valid === 'valid' || false,
89
+ onChange: this.handleValiditySelectionChange}),
90
+ el('label', {
91
+ key: 'validity-label',
92
+ htmlFor: 'conceptSearchValidity' }, 'valid only'),
93
+ ]),
94
+ )
95
+ }
96
+
97
+ searchControls.push(el('div', { key: 'refine', className: 'refine' }, refineControls));
98
+ }
99
+
100
+ return el(React.Fragment, null, searchControls);
101
+ }
102
+
103
+ emitSearchChange() {
104
+ this.props.onSearchChange({
105
+ valid: this.state.valid,
106
+ string: this.state.string,
107
+ });
108
+ }
109
+
110
+ handleSearchStringChange(evt) {
111
+ this.setState({ string: evt.target.value }, () => { this.emitSearchChange() });
112
+ }
113
+
114
+ handleValiditySelectionChange(evt) {
115
+ this.setState(({ valid, string }) => {
116
+ if (valid === 'valid') {
117
+ return { valid: undefined, string };
118
+ } else {
119
+ return { valid: 'valid', string };
120
+ }
121
+ }, () => { this.emitSearchChange() });
122
+ }
123
+ }
124
+
125
+ class ConceptList extends React.Component {
126
+ render() {
127
+ return el('table', null, [
128
+
129
+ el('thead', { key: 'thead' }, el('tr', null, this.props.fields.map((field) => {
130
+ return el('th', {
131
+ className: `field-${field.name}`,
132
+ key: field.name,
133
+ }, field.title);
134
+ }))),
135
+
136
+ el('tbody', { key: 'tbody' }, this.props.items.map((item) => {
137
+ const localizedItems = LANGUAGES.
138
+ filter((lang) => Object.keys(item).indexOf(lang) >= 0).
139
+ map((lang) => item[lang]);
140
+
141
+ return [item, ...localizedItems].map((item) => {
142
+ const isLocalized = item.hasOwnProperty('language_code');
143
+ const conceptId = isLocalized ? item.id : item.termid;
144
+
145
+ return el(
146
+ 'tr', {
147
+ key: `${conceptId}-${item.language_code}`,
148
+ className: `${isLocalized ? 'localized' : 'main'}`,
149
+ },
150
+ this.props.fields.map((field) => {
151
+ const view = field.view;
152
+ const defaultView = (item) => { return item[field.name]; };
153
+ return el(
154
+ 'td', {
155
+ className: `lang-${item.language_code} field-${field.name}`,
156
+ key: `${conceptId}-${item.language_code}-${field.name}`,
157
+ },
158
+ (view || defaultView)(item));
159
+ })
160
+ );
161
+ });
162
+ }).reduce((a, b) => a.concat(b), [])),
163
+
164
+ ]);
165
+ }
166
+ }
167
+
168
+ class ConceptBrowser extends React.Component {
169
+ constructor(props) {
170
+ super();
171
+
172
+ this.state = {
173
+ items: [],
174
+ searchQuery: {}, // string, (in future) valid
175
+ expanded: false,
176
+ error: false,
177
+ loading: false,
178
+ };
179
+
180
+ this.handleSearchQuery = this.handleSearchQuery.bind(this);
181
+ this.handleToggleBrowser = this.handleToggleBrowser.bind(this);
182
+ }
183
+
184
+ componentDidMount() {
185
+ searchWorker.onmessage = (msg) => {
186
+ if (msg.data.error) {
187
+ this.setState({ loading: false, error: msg.data.error });
188
+ } else {
189
+ this.setState({ loading: false, error: null, items: msg.data });
190
+ }
191
+ };
192
+ }
193
+
194
+ componentWillUnmount() {
195
+ searchWorker.onmessage = undefined;
196
+ }
197
+
198
+ render() {
199
+ var headerEls = [];
200
+ var searchString = this.state.searchQuery.string;
201
+
202
+ if (searchString && searchString.length > 1) {
203
+ let buttonLabel = this.state.expanded ? '×' : '+';
204
+ headerEls.push(
205
+ el('button', {
206
+ key: 'toggle',
207
+ ref: this.toggleSwitchRef,
208
+ className: 'toggle',
209
+ onClick: this.handleToggleBrowser,
210
+ }, buttonLabel)
211
+ );
212
+ }
213
+ headerEls.push(el('span', { key: 'title' }, 'Find a concept'));
214
+ headerEls.push(el('a', { key: 'link', href: '/concepts' }, '(browse all)'));
215
+
216
+ var els = [
217
+ el('h2', { key: 'section-title', className: 'section-title' }, headerEls),
218
+ el('div', { key: 'search-controls', className: 'search-controls' },
219
+ el(SearchControls, {
220
+ onSearchChange: this.handleSearchQuery,
221
+ refineControls: ['validity'],
222
+ })
223
+ ),
224
+ ];
225
+
226
+ if (this.state.error) {
227
+ els.push(el('div', {
228
+ key: 'search-results',
229
+ className: 'search-results status-message error',
230
+ dangerouslySetInnerHTML: { __html: this.state.error },
231
+ }));
232
+ } else if (this.state.loading) {
233
+ els.push(el('div', {
234
+ key: 'search-results',
235
+ className: 'search-results status-message loading',
236
+ }, 'Loading…'));
237
+ } else if (this.state.expanded) {
238
+ els.push(el('div', {
239
+ key: 'search-results',
240
+ className: 'search-results',
241
+ }, el(ConceptList, { items: this.state.items, fields })));
242
+ }
243
+
244
+ return el(React.Fragment, null, els);
245
+ }
246
+
247
+ handleSearchQuery(query) {
248
+ var hasQuery = query.string.length > 1;
249
+ if (hasQuery) {
250
+ window.setTimeout(() => { searchWorker.postMessage(query) }, 100);
251
+ }
252
+ this.setState({ loading: hasQuery, searchQuery: query, expanded: hasQuery });
253
+ updateBodyClass({ searchQuery: query, expanded: hasQuery });
254
+ }
255
+
256
+ handleToggleBrowser() {
257
+ this.setState((state) => {
258
+ state.expanded = !state.expanded;
259
+ updateBodyClass({ expanded: state.expanded });
260
+ return state;
261
+ });
262
+ }
263
+ }
264
+
265
+ ReactDOM.render(el(ConceptBrowser, null), document.querySelector('.browse-concepts'))
266
+
267
+ function getConceptPermalink(concept) {
268
+ if (concept.termid) {
269
+ return `/concepts/${concept.termid}/`;
270
+ } else if (concept.id && concept.language_code) {
271
+ return `/concepts/${concept.id}/#entry-lang-${concept.language_code}`;
272
+ } else {
273
+ return null;
274
+ }
275
+ }
276
+
277
+ function updateBodyClass({ searchQuery, expanded }) {
278
+ if (searchQuery) {
279
+ if (searchQuery.string.length > 1) {
280
+ document.querySelector('body').classList.add('browser-expandable');
281
+ } else {
282
+ document.querySelector('body').classList.remove('browser-expandable');
283
+ }
284
+ }
285
+
286
+ if (expanded === true) {
287
+ document.querySelector('body').classList.add('browser-expanded');
288
+ } else if (expanded === false) {
289
+ document.querySelector('body').classList.remove('browser-expanded');
290
+ }
291
+ }
292
+
293
+ }());