jekyll-geolexica 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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
+ }());