datalog-theme 0.10.1 → 0.11.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 (130) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +127 -0
  3. data/CITATION.cff +2 -2
  4. data/README.md +4 -2
  5. data/_data/i18n/en.yml +55 -3
  6. data/_data/i18n/es.yml +55 -3
  7. data/_data/i18n/pt.yml +55 -3
  8. data/_includes/analytics/dashboard.html +24 -9
  9. data/_includes/components/academic-dashboard.html +168 -133
  10. data/_includes/components/author-bio.html +7 -5
  11. data/_includes/components/author-list.html +6 -5
  12. data/_includes/components/citation-tools.html +2 -1
  13. data/_includes/components/content-provenance.html +19 -1
  14. data/_includes/components/correction-report.html +15 -8
  15. data/_includes/components/hero.html +22 -15
  16. data/_includes/components/package-index.html +121 -0
  17. data/_includes/components/package-install.html +27 -50
  18. data/_includes/components/post-list.html +14 -0
  19. data/_includes/components/responsive-image.html +15 -21
  20. data/_includes/footer.html +1 -1
  21. data/_includes/head.html +70 -11
  22. data/_includes/layouts/default/article.html +76 -46
  23. data/_includes/meta/dataset-json.html +171 -0
  24. data/_includes/meta/math-config.html +15 -1
  25. data/_includes/meta/package-json.html +69 -0
  26. data/_includes/meta/person-json.html +36 -13
  27. data/_includes/meta/publisher.html +15 -0
  28. data/_includes/meta/schema.html +108 -23
  29. data/_includes/meta/scholarly.html +10 -5
  30. data/_includes/post/related-posts.html +17 -43
  31. data/_includes/scripts.html +2 -2
  32. data/_includes/search/index-data.json +10 -1
  33. data/_includes/search/page.html +11 -6
  34. data/_layouts/archive.html +67 -0
  35. data/_layouts/dataset.html +2 -1
  36. data/_layouts/default.html +13 -11
  37. data/_layouts/docs.html +53 -0
  38. data/_layouts/home.html +30 -1
  39. data/_layouts/notebook.html +5 -1
  40. data/_layouts/package.html +38 -21
  41. data/_layouts/portfolio.html +1 -0
  42. data/_layouts/post.html +21 -62
  43. data/_layouts/project.html +2 -1
  44. data/_layouts/research.html +14 -2
  45. data/_plugins/analytics_dashboard.rb +4 -1
  46. data/_plugins/archive.rb +114 -0
  47. data/_plugins/authors.rb +50 -1
  48. data/_plugins/citation_exports.rb +12 -1
  49. data/_plugins/config_validator.rb +77 -2
  50. data/_plugins/correction_fallback.rb +110 -0
  51. data/_plugins/image_optimizer.rb +129 -22
  52. data/_plugins/math_preprocessor.rb +7 -48
  53. data/_plugins/notebook_converter.rb +44 -5
  54. data/_plugins/packages.rb +84 -0
  55. data/_plugins/page_dates.rb +37 -0
  56. data/_plugins/references.rb +16 -2
  57. data/_plugins/related_posts.rb +149 -0
  58. data/_plugins/search_sections.rb +110 -0
  59. data/_plugins/site_identity.rb +42 -0
  60. data/_plugins/social_cards.rb +17 -0
  61. data/_sass/_academic-dashboard.scss +18 -30
  62. data/_sass/_base.scss +2 -0
  63. data/_sass/_components.scss +21 -23
  64. data/_sass/_docs.scss +143 -0
  65. data/_sass/_features.scss +6 -0
  66. data/_sass/_layout.scss +65 -6
  67. data/_sass/_mathematical.scss +61 -19
  68. data/_sass/_notebooks.scss +1 -32
  69. data/_sass/_package-docs.scss +6 -9
  70. data/_sass/_post-components.scss +23 -23
  71. data/_sass/_print.scss +1 -1
  72. data/_sass/_search-page.scss +75 -0
  73. data/_sass/_search.scss +17 -26
  74. data/_sass/_syntax-highlighting.scss +23 -1
  75. data/_sass/_theme.scss +1 -0
  76. data/_sass/_utilities.scss +87 -40
  77. data/_sass/_variables.scss +31 -29
  78. data/assets/css/main.scss +2 -1
  79. data/assets/js/dist/academic.js +1 -1
  80. data/assets/js/dist/analytics-dashboard.js +1 -1
  81. data/assets/js/dist/chunks/chunk-TNVD6UAM.js +1 -0
  82. data/assets/js/dist/comments.js +1 -1
  83. data/assets/js/dist/contact.js +1 -1
  84. data/assets/js/dist/core.js +1 -1
  85. data/assets/js/dist/corrections.js +5 -1
  86. data/assets/js/dist/loader.js +1 -1
  87. data/assets/js/dist/math.js +2 -1
  88. data/assets/js/dist/moderation.js +1 -1
  89. data/assets/js/dist/reactions.js +1 -1
  90. data/assets/js/dist/search.js +1 -1
  91. data/assets/js/dist/sources.json +20 -16
  92. data/assets/js/dist/subscriptions.js +1 -1
  93. data/assets/js/loader.js +6 -2
  94. data/lib/datalog/audit/checks.rb +269 -0
  95. data/lib/datalog/audit/known_keys.rb +61 -0
  96. data/lib/datalog/audit/site_reader.rb +118 -0
  97. data/lib/datalog/audit/source_file.rb +85 -0
  98. data/lib/datalog/audit.rb +145 -0
  99. data/lib/datalog/citations/bibtex.rb +200 -0
  100. data/lib/datalog/citations/entry.rb +333 -0
  101. data/lib/datalog/citations/markup.rb +79 -0
  102. data/lib/datalog/cli.rb +164 -89
  103. data/lib/datalog/critical_css.rb +126 -21
  104. data/lib/datalog/latex_speech/words.json +88 -0
  105. data/lib/datalog/latex_speech.rb +355 -0
  106. data/lib/datalog/packages/command.rb +51 -0
  107. data/lib/datalog/packages/refresh.rb +187 -0
  108. data/lib/datalog/packages.rb +177 -0
  109. data/lib/datalog/plugin_system.rb +5 -0
  110. data/lib/datalog/plugins/citations.rb +325 -109
  111. data/lib/datalog/site_config.rb +26 -0
  112. data/lib/datalog/social_cards/font.rb +286 -0
  113. data/lib/datalog/social_cards/fonts/IBMPlexSans-Regular.ttf +0 -0
  114. data/lib/datalog/social_cards/fonts/IBMPlexSerif-SemiBold.ttf +0 -0
  115. data/lib/datalog/social_cards/fonts/OFL.txt +92 -0
  116. data/lib/datalog/social_cards/geometry.rb +51 -0
  117. data/lib/datalog/social_cards/outline.rb +54 -0
  118. data/lib/datalog/social_cards/plain_text.rb +258 -0
  119. data/lib/datalog/social_cards/template.rb +322 -0
  120. data/lib/datalog/social_cards/template.svg +17 -0
  121. data/lib/datalog/social_cards/typesetter.rb +178 -0
  122. data/lib/datalog/social_cards.rb +369 -0
  123. data/lib/datalog/theme/updater.rb +307 -0
  124. data/lib/datalog/theme/version.rb +1 -1
  125. metadata +44 -7
  126. data/_includes/components/enhanced-code-block.html +0 -212
  127. data/_includes/components/performance-monitor.html +0 -170
  128. data/_includes/components/viz-table-fallback.html +0 -19
  129. data/_plugins/datalog_bibliography.rb +0 -21
  130. data/assets/js/dist/chunks/chunk-WIRUK3OZ.js +0 -1
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  layout: default
3
3
  show_title: false # this layout renders the page's <h1>
4
+ article_meta: false # the default heading adds no date or byline above this layout's own header
4
5
  ---
5
6
 
6
7
  {%- comment -%}
@@ -24,10 +25,13 @@ show_title: false # this layout renders the page's <h1>
24
25
  <p class="notebook-article__summary">{{ page.summary | default: page.excerpt }}</p>
25
26
 
26
27
  <ul class="notebook-article__meta">
28
+ {%- assign notebook_published = page | published_date %}
29
+ {%- if notebook_published %}
27
30
  <li>
28
31
  <span>{% t 'notebook.meta.published' %}</span>
29
- <time datetime="{{ page.date | date_to_xmlschema }}" itemprop="datePublished">{{ page.date | localize_date: date_format }}</time>
32
+ <time datetime="{{ notebook_published | date_to_xmlschema }}" itemprop="datePublished">{{ notebook_published | localize_date: date_format }}</time>
30
33
  </li>
34
+ {%- endif %}
31
35
 
32
36
  {% if page.last_modified_at %}
33
37
  <li>
@@ -1,8 +1,16 @@
1
1
  ---
2
2
  layout: default
3
3
  show_title: false # this layout renders the page's <h1>
4
+ article_meta: false # the default heading adds no date or byline above this layout's own header
4
5
  ---
5
6
 
7
+ {%- comment -%}
8
+ The latest release: from _data/package_releases.yml, which
9
+ `datalog packages refresh` writes from the package's registries, else from
10
+ the front matter. The registries' pages and docs.rs come from `registry:`.
11
+ {%- endcomment -%}
12
+ {%- assign release = page | package_release -%}
13
+ {%- assign registry_links = page | package_links -%}
6
14
  <div class="package-docs">
7
15
  {%- comment -%} Package Header {%- endcomment -%}
8
16
  <div class="package-docs__header">
@@ -20,33 +28,42 @@ show_title: false # this layout renders the page's <h1>
20
28
  </div>
21
29
 
22
30
  <div class="package-docs__meta">
23
- {% if page.version %}
24
- <div class="package-docs__version">
25
- <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
31
+ {% if release.version %}
32
+ <div class="package-docs__version" data-package-version>
33
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
26
34
  <circle cx="12" cy="12" r="10"></circle>
27
35
  <polyline points="12 6 12 12 16 14"></polyline>
28
36
  </svg>
29
- <span>v{{ page.version }}</span>
37
+ <span>v{{ release.version | escape }}{% if release.prerelease %} · {% t 'packages.prerelease' %}{% endif %}{% if release.yanked %} · {% t 'packages.yanked' %}{% endif %}</span>
38
+ {%- if release.released %}
39
+ <span>· <time datetime="{{ release.released | escape }}">{{ release.released | localize_date: 'long' }}</time></span>
40
+ {%- endif %}
30
41
  </div>
31
42
  {% endif %}
32
43
 
33
- {% if page.language %}
44
+ {% if release.requirement or page.language %}
34
45
  <div class="package-docs__language">
35
- <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
46
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
36
47
  <polyline points="16 18 22 12 16 6"></polyline>
37
48
  <polyline points="8 6 2 12 8 18"></polyline>
38
49
  </svg>
39
- <span>{{ page.language }}</span>
50
+ <span>{{ release.requirement | default: page.language | escape }}</span>
40
51
  </div>
41
52
  {% endif %}
42
53
 
43
- {% if page.license %}
54
+ {% if release.license %}
44
55
  <div class="package-docs__license">
45
- <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
56
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
46
57
  <rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
47
58
  <path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
48
59
  </svg>
49
- <span>{{ page.license }}</span>
60
+ <span>{{ release.license | escape }}</span>
61
+ </div>
62
+ {% endif %}
63
+
64
+ {% if release.status %}
65
+ <div class="package-docs__status">
66
+ <span>{{ release.status | capitalize | escape }}</span>
50
67
  </div>
51
68
  {% endif %}
52
69
  </div>
@@ -61,22 +78,22 @@ show_title: false # this layout renders the page's <h1>
61
78
  </a>
62
79
  {% endif %}
63
80
 
64
- {% if page.pypi_url %}
65
- <a href="{{ page.pypi_url }}" class="package-docs__action-btn" target="_blank" rel="noopener">
66
- <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
81
+ {%- comment -%} PyPI, crates.io and docs.rs, RubyGems, CRAN, conda-forge, npm, JuliaHub {%- endcomment -%}
82
+ {% for link in registry_links %}
83
+ <a href="{{ link.url | escape }}" class="package-docs__action-btn" target="_blank" rel="noopener" data-registry="{{ link.registry }}">
84
+ <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
67
85
  <path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path>
68
86
  </svg>
69
- <span>PyPI</span>
87
+ <span>{{ link.label }}</span>
70
88
  </a>
71
- {% endif %}
89
+ {% endfor %}
72
90
 
73
- {% if page.cran_url %}
74
- <a href="{{ page.cran_url }}" class="package-docs__action-btn" target="_blank" rel="noopener">
75
- <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
76
- <circle cx="12" cy="12" r="10"></circle>
77
- <text x="12" y="16" font-size="12" text-anchor="middle" fill="currentColor">R</text>
91
+ {% if page.docs_url %}
92
+ <a href="{{ page.docs_url | escape }}" class="package-docs__action-btn" target="_blank" rel="noopener" data-registry="docs">
93
+ <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
94
+ <path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20V3H6.5A2.5 2.5 0 0 0 4 5.5z"></path>
78
95
  </svg>
79
- <span>CRAN</span>
96
+ <span>{% t 'packages.documentation' %}</span>
80
97
  </a>
81
98
  {% endif %}
82
99
  </div>
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  layout: default
3
3
  show_title: false # this layout renders the page's <h1>
4
+ article_meta: false # the default heading adds no date or byline above this layout's own header
4
5
  ---
5
6
  <article class="portfolio-item">
6
7
  <header class="portfolio-header">
data/_layouts/post.html CHANGED
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  layout: default
3
3
  schema_type: TechArticle
4
+ article_meta: false # the default heading adds no date or byline above this layout's own header
4
5
  ---
5
6
 
6
7
  {% assign localization_config = site.theme_options.localization %}
@@ -48,6 +49,15 @@ schema_type: TechArticle
48
49
  {% assign has_categories = false %}
49
50
  {% if page.categories and page.categories.size > 0 %}{% assign has_categories = true %}{% endif %}
50
51
 
52
+ {%- comment -%}
53
+ Where the author and editorial note goes: after the article by default
54
+ (theme_options.provenance.position: end), or before it with `start`. A page
55
+ chooses for itself with provenance_position in its front matter.
56
+ {%- endcomment -%}
57
+ {% assign provenance_position = page.provenance_position | default: site.theme_options.provenance.position | default: 'end' %}
58
+ {% assign provenance_first = false %}
59
+ {% if provenance_position == 'start' %}{% assign provenance_first = true %}{% endif %}
60
+
51
61
  {%- comment -%} ISO 8601 duration for reading time {%- endcomment -%}
52
62
  {% assign time_required = 'PT' | append: reading_minutes | append: 'M' %}
53
63
 
@@ -55,6 +65,7 @@ schema_type: TechArticle
55
65
  {% include components/breadcrumbs.html %}
56
66
 
57
67
  <div class="post-wrapper" itemscope itemtype="https://schema.org/TechArticle">
68
+ <meta itemprop="headline" content="{{ page.title | escape }}" />
58
69
  <meta itemprop="url" content="{{ canonical_url }}" />
59
70
  <meta itemprop="datePublished" content="{{ page.date | date_to_xmlschema }}" />
60
71
  {% if page.last_modified_at or page.updated %}
@@ -123,6 +134,8 @@ schema_type: TechArticle
123
134
  </dd>
124
135
  </div>
125
136
  {% endif %}
137
+ {%- comment -%} With the note after the article, the metadata says there is one, and when the article was reviewed {%- endcomment -%}
138
+ {% unless provenance_first %}{% include components/content-provenance.html page=page date_format=date_format pointer=true %}{% endunless %}
126
139
  </dl>
127
140
  </section>
128
141
 
@@ -157,8 +170,8 @@ schema_type: TechArticle
157
170
  </section>
158
171
  {% endif %}
159
172
 
160
- {%- comment -%} Editorial provenance (why_this_exists, evidence, methodology, reviewed_at) {%- endcomment -%}
161
- {% include components/content-provenance.html page=page date_format=date_format %}
173
+ {%- comment -%} Editorial provenance (why_this_exists, evidence, methodology, reviewed_at), here with provenance position `start` {%- endcomment -%}
174
+ {% if provenance_first %}{% include components/content-provenance.html page=page date_format=date_format %}{% endif %}
162
175
 
163
176
  {%- comment -%} The article's series: every part in order, and the previous and next ones {%- endcomment -%}
164
177
  {% include components/series-nav.html page=page %}
@@ -189,6 +202,9 @@ schema_type: TechArticle
189
202
  {%- comment -%} The code, data and environment behind the analysis {%- endcomment -%}
190
203
  {% include components/reproducibility.html page=page %}
191
204
 
205
+ {%- comment -%} How the article was made: after it, by default, as the apparatus that follows the text {%- endcomment -%}
206
+ {% unless provenance_first %}{% include components/content-provenance.html page=page date_format=date_format %}{% endunless %}
207
+
192
208
  {%- comment -%} Report an error: structured feedback to the author, on a site with a backend {%- endcomment -%}
193
209
  {% include components/correction-report.html %}
194
210
 
@@ -281,8 +297,9 @@ schema_type: TechArticle
281
297
  {% assign subscribe_placement = site.subscriptions.placement | default: 'footer' %}
282
298
  {% if subscribe_placement contains 'post' %}{% include components/subscribe-form.html where='post' %}{% endif %}
283
299
 
284
- {%- comment -%} ===== Robust related posts (no where_exp, no nil sort) ===== {%- endcomment -%}
285
- {% include post/related-posts.html page=page date_format=date_format %}
300
+ {%- unless page.related_posts == false -%}
301
+ {% include post/related-posts.html page=page date_format=date_format %}
302
+ {%- endunless -%}
286
303
 
287
304
  <nav class="post-pagination" aria-label="Post navigation">
288
305
  {% if page.previous %}
@@ -326,65 +343,7 @@ schema_type: TechArticle
326
343
  </div>
327
344
 
328
345
  <script nonce="{{ page.csp_nonce }}">
329
- // Enhance code blocks with a copy button and language badge.
330
346
  document.addEventListener('DOMContentLoaded', function () {
331
- const codeBlocks = document.querySelectorAll('.post-content pre > code');
332
- codeBlocks.forEach(function (codeBlock) {
333
- const pre = codeBlock.parentElement;
334
- if (!pre || pre.querySelector('.code-copy')) return;
335
-
336
- const wrapper = document.createElement('div');
337
- wrapper.className = 'code-block-wrapper';
338
- pre.parentNode.insertBefore(wrapper, pre);
339
- wrapper.appendChild(pre);
340
-
341
- const metaBar = document.createElement('div');
342
- metaBar.className = 'code-block-meta';
343
-
344
- // Rouge puts the language on the block's wrapper rather than on the code.
345
- // With line numbers on, the code is a table whose first column holds the
346
- // numbers, and the copy leaves them out.
347
- const langHolder = codeBlock.matches('[class*="language-"]') ? codeBlock : codeBlock.closest('.highlighter-rouge');
348
- const lang = langHolder && langHolder.getAttribute('class');
349
- const source = codeBlock.querySelector('.rouge-code') || codeBlock;
350
- if (lang) {
351
- const m = lang.match(/language-([a-z0-9+#]+)/i);
352
- if (m) {
353
- const langBadge = document.createElement('span');
354
- langBadge.className = 'code-language';
355
- langBadge.textContent = m[1].toUpperCase();
356
- metaBar.appendChild(langBadge);
357
- }
358
- }
359
-
360
- const button = document.createElement('button');
361
- button.type = 'button';
362
- button.className = 'code-copy';
363
- button.setAttribute('aria-label', 'Copy code to clipboard');
364
- button.textContent = 'Copy';
365
-
366
- if (navigator.clipboard && navigator.clipboard.writeText) {
367
- button.addEventListener('click', function () {
368
- navigator.clipboard.writeText(source.textContent).then(function () {
369
- button.textContent = 'Copied!';
370
- button.classList.add('is-copied');
371
- setTimeout(function () {
372
- button.textContent = 'Copy';
373
- button.classList.remove('is-copied');
374
- }, 2000);
375
- }).catch(function () {
376
- button.textContent = 'Error';
377
- });
378
- });
379
- } else {
380
- button.disabled = true;
381
- button.title = 'Clipboard copying is not supported in this browser';
382
- }
383
-
384
- metaBar.appendChild(button);
385
- wrapper.insertBefore(metaBar, pre);
386
- });
387
-
388
347
  // Make equation references focus their targets for accessibility. This runs
389
348
  // on DOMContentLoaded, before MathJax has loaded, when window.MathJax is
390
349
  // still the configuration object and has no startup.promise; reading it
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  layout: default
3
3
  show_title: false # this layout renders the page's <h1>
4
+ article_meta: false # the default heading adds no date or byline above this layout's own header
4
5
  ---
5
6
  {% assign localization_config = site.theme_options.localization %}
6
7
  {% assign date_format = localization_config.date_format | default: 'long' %}
@@ -210,7 +211,7 @@ show_title: false # this layout renders the page's <h1>
210
211
 
211
212
  {% assign citation_key = page.citation_key | default: page.slug | default: page.title | slugify %}
212
213
  {% assign citation_authors = page | page_authors %}
213
- {% assign citation_date = page.date | default: page.last_updated %}
214
+ {% assign citation_date = page | published_date | default: page.last_updated %}
214
215
  {% assign citation_url = page.url | absolute_url %}
215
216
  {% include components/citation-tools.html title=page.title authors=citation_authors date=citation_date url=citation_url citation_key=citation_key citation_type='software' doi=page.doi %}
216
217
 
@@ -1,6 +1,8 @@
1
1
  ---
2
2
  layout: default
3
3
  schema_type: ScholarlyArticle
4
+ show_title: false # this layout renders the page's <h1>
5
+ article_meta: false # the default heading adds no date or byline above this layout's own header
4
6
  ---
5
7
  {% assign date_format = site.date_format | default: '%B %-d, %Y' %}
6
8
  {% assign authors = page | page_authors %}
@@ -32,6 +34,7 @@ schema_type: ScholarlyArticle
32
34
  {% assign supplementary = page.supplementary | default: page.supplementary_materials %}
33
35
  {% assign references = page.references %}
34
36
  {% assign references_array = references | arrayify %}
37
+ {% capture bibliography_label %}{% t 'post.bibliography.label' %}{% endcapture %}
35
38
  {% assign funding = page.funding %}
36
39
  {% assign rights = page.rights %}
37
40
  {% assign research_license = page | page_license %}
@@ -57,6 +60,9 @@ schema_type: ScholarlyArticle
57
60
 
58
61
  <header class="research-header" aria-labelledby="research-title">
59
62
  <h1 id="research-title" class="research-title">{{ page.title | escape }}</h1>
63
+ {%- if page.summary %}
64
+ <p class="article-summary">{{ page.summary | strip_html | escape }}</p>
65
+ {%- endif %}
60
66
 
61
67
  <div class="research-authors" aria-label="Authors">
62
68
  <h2 class="visually-hidden">Authors</h2>
@@ -73,7 +79,7 @@ schema_type: ScholarlyArticle
73
79
  {% endif %}
74
80
  {% if author_orcid %}
75
81
  <span class="research-author-orcid">
76
- <a href="{{ author_orcid }}" target="_blank" rel="noopener noreferrer" itemprop="identifier" aria-label="ORCID profile for {{ author_name }}">
82
+ <a href="{{ author_orcid }}" target="_blank" rel="{{ author | author_profile_rel | escape }}" itemprop="identifier" aria-label="ORCID profile for {{ author_name }}">
77
83
  ORCID
78
84
  </a>
79
85
  </span>
@@ -302,7 +308,13 @@ schema_type: ScholarlyArticle
302
308
  </section>
303
309
  {% endif %}
304
310
 
305
- {% if references_array and references_array.size > 0 %}
311
+ {%- comment -%} The works the article cites with {% raw %}{% cite %}{% endraw %}; a hand-written `references:` list otherwise {%- endcomment -%}
312
+ {% if page.datalog_bibliography and page.datalog_bibliography.size > 0 %}
313
+ <section class="research-references" aria-labelledby="references-heading">
314
+ <h2 id="references-heading">{{ page.datalog_bibliography_title | default: bibliography_label }}</h2>
315
+ {% datalog_bibliography %}
316
+ </section>
317
+ {% elsif references_array and references_array.size > 0 %}
306
318
  <section class="research-references" aria-labelledby="references-heading">
307
319
  <h2 id="references-heading">References</h2>
308
320
  {% assign references_markup = references_array | join: '' %}
@@ -25,12 +25,15 @@ module Datalog
25
25
  module_function
26
26
 
27
27
  def fetch(site)
28
+ cached = nil
28
29
  cache_path = cache_path_for(site)
29
30
  cached = read_cache(cache_path)
30
31
  return cached if fresh?(cached)
31
32
 
32
33
  data = query_analytics(site)
33
34
  data["fetched_at"] = Time.now.utc.iso8601
35
+ return cached.merge("stale" => true) if data["status"] == "error" && cached&.fetch("status", nil) == "ok"
36
+
34
37
  # Only a successful report is cached. A missing-configuration or error
35
38
  # payload used to be kept for CACHE_TTL as well, so a site that had just
36
39
  # set GA4_PROPERTY_ID went on reporting the old problem for a day.
@@ -38,7 +41,7 @@ module Datalog
38
41
  data
39
42
  rescue StandardError => e
40
43
  Jekyll.logger.warn("Analytics", "Falling back to cached analytics data: #{e.message}")
41
- cached || fallback_payload("error", e.message)
44
+ cached&.fetch("status", nil) == "ok" ? cached.merge("stale" => true) : fallback_payload("error", e.message)
42
45
  end
43
46
 
44
47
  def cache_path_for(site)
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datalog
4
+ module Archive
5
+ module_function
6
+
7
+ def build(site, page)
8
+ mode = page["archive"].to_s
9
+ limit = (page["archive_limit"] || site.config["archive_limit"]).to_i
10
+ limit = 20 if limit <= 0
11
+ posts = site.posts.docs.sort_by { |post| [-post.date.to_i, post.url] }
12
+
13
+ case mode
14
+ when "year" then years(posts, page, limit)
15
+ when "tag" then terms(posts, "tags", limit)
16
+ when "category" then terms(posts, "categories", limit)
17
+ when "facet" then facet(posts, page, limit)
18
+ when "topic" then topic(posts, page, limit)
19
+ else
20
+ []
21
+ end
22
+ end
23
+
24
+ def years(posts, page, limit)
25
+ groups = grouped(posts, limit) { |post| [[post.date.year.to_s, post.date.year.to_s]] }
26
+ groups.sort_by! { |group| -group["name"].to_i }
27
+ return groups unless page["archive_months"]
28
+
29
+ groups.each do |year|
30
+ year_posts = posts.select { |post| post.date.year.to_s == year["name"] }
31
+ months = grouped(year_posts, limit) do |post|
32
+ [[post.date.strftime("%Y-%m"), post.date.strftime("%Y-%m")]]
33
+ end
34
+ year["months"] = months.sort_by { |month| month["name"] }.reverse
35
+ end
36
+ groups
37
+ end
38
+
39
+ def terms(posts, field, limit)
40
+ groups = grouped(posts, limit) do |post|
41
+ Array(post.data[field]).map { |term| [term.to_s, term.to_s] }
42
+ end
43
+ groups.sort_by { |group| group["name"].downcase }
44
+ end
45
+
46
+ def facet(posts, page, limit)
47
+ field = page["archive_field"].to_s
48
+ wanted = Array(page["archive_terms"]).map(&:to_s)
49
+ groups = grouped(posts, limit) do |post|
50
+ Array(post.data[field]).map { |term| [term.to_s, term.to_s] }.select do |name, _|
51
+ wanted.any? { |given| given.casecmp?(name) }
52
+ end
53
+ end
54
+ wanted.filter_map { |term| groups.find { |group| group["name"].casecmp?(term) } }
55
+ end
56
+
57
+ def grouped(posts, limit)
58
+ groups = {}
59
+ posts.each do |post|
60
+ yield(post).each do |name, label|
61
+ slug = Jekyll::Utils.slugify(name)
62
+ next if slug.empty?
63
+
64
+ group = (groups[slug] ||= { "name" => label, "slug" => slug, "posts" => [] })
65
+ group["posts"] << post unless group["posts"].include?(post)
66
+ end
67
+ end
68
+ groups.each_value do |group|
69
+ group["count"] = group["posts"].size
70
+ group["remaining"] = group["posts"].drop(limit)
71
+ group["posts"] = group["posts"].first(limit)
72
+ end
73
+ groups.values
74
+ end
75
+
76
+ def topic(posts, page, limit)
77
+ config = page["topic"] || {}
78
+ tags = Array(config["tags"]).map { |value| value.to_s.downcase }
79
+ categories = Array(config["categories"]).map { |value| value.to_s.downcase }
80
+ featured = Array(config["featured"]).map(&:to_s)
81
+ matches = posts.select { |post| topic_match?(post, tags, categories) }
82
+ series, loose = matches.partition do |post|
83
+ post.data["series"].is_a?(Hash) && post.data["series"]["id"]
84
+ end
85
+ series = topic_series(series, featured)
86
+ loose.sort_by! { |post| [featured.include?(post.url) ? 0 : 1, -post.date.to_i, post.url] }
87
+ { "series" => series, "posts" => loose.first(limit), "remaining" => loose.drop(limit),
88
+ "count" => matches.size }
89
+ end
90
+
91
+ def topic_match?(post, tags, categories)
92
+ Array(post.data["tags"]).map { |value| value.to_s.downcase }.intersect?(tags) ||
93
+ Array(post.data["categories"]).map { |value| value.to_s.downcase }.intersect?(categories)
94
+ end
95
+
96
+ def topic_series(posts, featured)
97
+ groups = posts.group_by { |post| post.data["series"]["id"] }
98
+ series = groups.map do |_id, parts|
99
+ data = parts.first.data["series"]
100
+ { "title" => data["title"], "url" => parts.first.url,
101
+ "count" => parts.size, "featured" => parts.any? { |post| featured.include?(post.url) } }
102
+ end
103
+ series.sort_by { |item| [item["featured"] ? 0 : 1, item["title"].to_s.downcase] }
104
+ end
105
+ end
106
+
107
+ module ArchiveFilters
108
+ def archive_data(page)
109
+ Archive.build(@context.registers[:site], page)
110
+ end
111
+ end
112
+ end
113
+
114
+ Liquid::Template.register_filter(Datalog::ArchiveFilters)
data/_plugins/authors.rb CHANGED
@@ -22,6 +22,8 @@ module Datalog
22
22
  }.freeze
23
23
  PROFILE_LINKS = %w[researchgate google_scholar].freeze
24
24
  ORCID_ID = /\A\d{4}-\d{4}-\d{4}-\d{3}[\dX]\z/
25
+ # Profile links open in a new tab; no `profile_rel` setting removes these.
26
+ SAFE_REL = %w[noopener noreferrer].freeze
25
27
 
26
28
  # `authors:`, else `author:`, else the site's author.
27
29
  def authors(page, site)
@@ -94,6 +96,10 @@ module Datalog
94
96
  record["affiliation"] ||= record["institution"]
95
97
  record["bio"] ||= record["biography"]
96
98
  record["image"] ||= record["avatar"] || record["photo"]
99
+ # What the JSON-LD Person says about them besides the name, each a list.
100
+ record["alternate_names"] = list(record, "alternate_names", "alternate_name")
101
+ record["job_titles"] = list(record, "job_title", "roles")
102
+ record["knows_about"] = list(record, "knows_about", "expertise", "research_areas")
97
103
  record["orcid"] = "https://orcid.org/#{record['orcid']}" if record["orcid"].to_s.match?(ORCID_ID)
98
104
  record["site_author"] = record["name"] == site_author_profile(site)&.fetch("name")
99
105
  PROFILE_URLS.each do |key, template|
@@ -107,11 +113,44 @@ module Datalog
107
113
  record
108
114
  end
109
115
 
116
+ # The `rel` of a person's profile links. The site's own author gets `me`,
117
+ # which says the profile belongs to whoever owns the site: the relationship
118
+ # the JSON-LD `sameAs` states, in the form IndieAuth and Mastodon's
119
+ # verified links read. Anyone else gets it only when their record sets
120
+ # `profile_rel`, so a guest is never claimed as the site's owner. A record's
121
+ # `profile_rel` replaces `me`, and `author.profile_rel` in _config.yml does
122
+ # so for the site author; neither can drop noopener or noreferrer.
123
+ def profile_rel(person, site)
124
+ record = person.is_a?(String) ? { "name" => person } : present(person)
125
+ owner = site_author?(record, site)
126
+ configured = record["profile_rel"]
127
+ configured = site_author_profile(site)&.fetch("profile_rel", nil) if configured.nil? && owner
128
+ tokens = configured.to_s.downcase.split
129
+ tokens = ["me"] if configured.nil? && owner
130
+ (tokens + SAFE_REL).uniq.join(" ")
131
+ end
132
+
133
+ # A record from `page_authors` knows; a raw data record is compared by name.
134
+ def site_author?(record, site)
135
+ return record["site_author"] == true if record.key?("site_author")
136
+
137
+ name = site_author_profile(site)&.fetch("name")
138
+ !name.nil? && record["name"] == name
139
+ end
140
+
141
+ # The profiles the record names, then any other address it lists in `same_as`.
110
142
  def profiles(record)
111
143
  urls = [record["orcid"]]
112
144
  urls += PROFILE_URLS.map { |key, template| format(template, record[key]) if record[key] }
113
145
  urls += PROFILE_LINKS.map { |key| record[key] }
114
- urls.compact.map(&:to_s).reject(&:empty?).uniq
146
+ urls += Array(record["same_as"])
147
+ urls.compact.map { |url| url.to_s.strip }.reject(&:empty?).uniq
148
+ end
149
+
150
+ # The first of `keys` the record sets, as a list of its non-blank values.
151
+ def list(record, *keys)
152
+ found = keys.map { |key| record[key] }.compact.first
153
+ Array(found).map { |item| item.to_s.strip }.reject(&:empty?)
115
154
  end
116
155
 
117
156
  # `author_affiliation` in front matter names the affiliation of a page's
@@ -145,6 +184,16 @@ module Datalog
145
184
  def page_contributors(page)
146
185
  Authors.contributors(page, @context["site"])
147
186
  end
187
+
188
+ def author_profile_rel(person)
189
+ Authors.profile_rel(person, @context["site"])
190
+ end
191
+
192
+ # The site's own author as one full record, whatever the input:
193
+ # `{% assign owner = site | site_author_record %}`.
194
+ def site_author_record(_input = nil)
195
+ Authors.site_author(@context["site"])
196
+ end
148
197
  end
149
198
  end
150
199
 
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "page_dates"
4
+
3
5
  module Datalog
4
6
  module CitationExports
5
7
  module_function
@@ -79,7 +81,7 @@ module Datalog
79
81
  key = slugify(get.call("citation_key") || Authors.value(page, "slug") || title)
80
82
  kind = get.call("citation_type") || Authors.value(options, "type") || "article"
81
83
  record = {
82
- "title" => title, "year" => (date(get.call("date"), "%Y") if get.call("date")),
84
+ "title" => title, "year" => citation_year(page, options),
83
85
  "publisher" => publisher, "journal" => get.call("journal") || get.call("publication"),
84
86
  "doi" => get.call("doi"), "url" => absolute_url(get.call("url") || "/"),
85
87
  "volume" => get.call("volume"), "number" => get.call("issue") || get.call("number"),
@@ -87,6 +89,15 @@ module Datalog
87
89
  }
88
90
  CitationExports.render(record, authors, kind, key)
89
91
  end
92
+
93
+ private
94
+
95
+ # The year of the date given, else of the page's own. An undated page's
96
+ # date is the build's, and gives no year (#411).
97
+ def citation_year(page, options)
98
+ published = Authors.value(options, "date") || PageDates.published(page, @context.registers[:site]&.time)
99
+ date(published, "%Y") if published
100
+ end
90
101
  end
91
102
  end
92
103