datalog-theme 0.8.0 → 0.9.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 (111) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +63 -0
  3. data/CITATION.cff +2 -2
  4. data/README.md +4 -2
  5. data/_data/i18n/en.yml +297 -0
  6. data/_data/i18n/es.yml +297 -0
  7. data/_data/i18n/pt.yml +297 -0
  8. data/_data/js_manifest.json +16 -0
  9. data/_includes/components/author-bio.html +21 -11
  10. data/_includes/components/author-list.html +32 -0
  11. data/_includes/components/citation-tools.html +33 -25
  12. data/_includes/components/comments-thread.html +90 -0
  13. data/_includes/components/contact-form.html +120 -0
  14. data/_includes/components/correction-report.html +82 -0
  15. data/_includes/components/enhanced-toc.html +6 -7
  16. data/_includes/components/license-link.html +13 -0
  17. data/_includes/components/license-notice.html +28 -0
  18. data/_includes/components/moderation-inbox.html +139 -0
  19. data/_includes/components/reactions.html +49 -0
  20. data/_includes/components/reading-list.html +31 -0
  21. data/_includes/components/reading-mode-toggle.html +65 -0
  22. data/_includes/components/reading-state-bookmark.html +27 -0
  23. data/_includes/components/reading-state-panel.html +71 -0
  24. data/_includes/components/reproducibility.html +61 -0
  25. data/_includes/components/responsive-image.html +3 -3
  26. data/_includes/components/revision-history.html +39 -0
  27. data/_includes/components/revision-notice.html +35 -0
  28. data/_includes/components/series-nav.html +64 -0
  29. data/_includes/components/subscribe-form.html +88 -0
  30. data/_includes/components/subscription-manage.html +68 -0
  31. data/_includes/components/webmentions.html +48 -0
  32. data/_includes/csp-meta.html +21 -1
  33. data/_includes/footer.html +3 -0
  34. data/_includes/head.html +51 -40
  35. data/_includes/layouts/default/article.html +4 -6
  36. data/_includes/meta/dynamic-services-config.html +14 -0
  37. data/_includes/meta/math-config.html +9 -5
  38. data/_includes/meta/person-json.html +27 -0
  39. data/_includes/meta/publisher.html +45 -0
  40. data/_includes/meta/schema.html +62 -28
  41. data/_includes/meta/scholarly.html +121 -0
  42. data/_includes/meta/scripts-loader.html +2 -0
  43. data/_includes/meta/webmention-discovery.html +14 -0
  44. data/_includes/scripts.html +57 -0
  45. data/_layouts/dataset.html +4 -3
  46. data/_layouts/default.html +1 -1
  47. data/_layouts/package.html +3 -2
  48. data/_layouts/page.html +15 -0
  49. data/_layouts/post.html +82 -18
  50. data/_layouts/project.html +2 -2
  51. data/_layouts/research.html +20 -8
  52. data/_plugins/authors.rb +133 -0
  53. data/_plugins/config_validator.rb +223 -20
  54. data/_plugins/critical_css_check.rb +42 -0
  55. data/_plugins/i18n.rb +6 -4
  56. data/_plugins/image_optimizer.rb +224 -172
  57. data/_plugins/licenses.rb +135 -0
  58. data/_plugins/math_preprocessor.rb +32 -10
  59. data/_plugins/references.rb +238 -0
  60. data/_plugins/reproducibility.rb +150 -0
  61. data/_plugins/revisions.rb +101 -0
  62. data/_plugins/scholarly.rb +50 -0
  63. data/_plugins/series.rb +104 -0
  64. data/_plugins/statements.rb +87 -0
  65. data/_sass/_base.scss +8 -1
  66. data/_sass/_comments-thread.scss +159 -0
  67. data/_sass/_layout.scss +384 -0
  68. data/_sass/_mathematical.scss +27 -0
  69. data/_sass/_moderation.scss +222 -0
  70. data/_sass/_post-components.scss +4 -2
  71. data/_sass/_print.scss +291 -0
  72. data/_sass/_reactions.scss +89 -0
  73. data/_sass/_reading-state.scss +290 -0
  74. data/_sass/_service-forms.scss +204 -0
  75. data/_sass/_subscriptions.scss +140 -0
  76. data/_sass/_syntax-highlighting.scss +2 -2
  77. data/_sass/_theme.scss +11 -0
  78. data/_sass/_typography.scss +116 -0
  79. data/_sass/_utilities.scss +5 -0
  80. data/_sass/_variables.scss +6 -0
  81. data/_sass/_webmentions.scss +125 -0
  82. data/assets/js/dist/academic.js +1 -1
  83. data/assets/js/dist/analytics-dashboard.js +1 -1
  84. data/assets/js/dist/chunks/chunk-2DYDWUFX.js +1 -0
  85. data/assets/js/dist/chunks/chunk-PATLC23F.js +1 -0
  86. data/assets/js/dist/chunks/chunk-V7734B2G.js +1 -0
  87. data/assets/js/dist/comments.js +2 -0
  88. data/assets/js/dist/contact.js +1 -0
  89. data/assets/js/dist/core.js +1 -1
  90. data/assets/js/dist/corrections.js +1 -0
  91. data/assets/js/dist/loader.js +1 -1
  92. data/assets/js/dist/math.js +1 -1
  93. data/assets/js/dist/moderation.js +1 -0
  94. data/assets/js/dist/notebook.js +1 -1
  95. data/assets/js/dist/reactions.js +1 -0
  96. data/assets/js/dist/reading-state.js +1 -0
  97. data/assets/js/dist/search.js +1 -1
  98. data/assets/js/dist/sources.json +52 -0
  99. data/assets/js/dist/subscriptions.js +1 -0
  100. data/assets/js/dist/webmentions.js +1 -0
  101. data/assets/js/loader.js +34 -0
  102. data/datalog-theme.gemspec +1 -2
  103. data/lib/datalog/cli.rb +9 -1
  104. data/lib/datalog/critical_css.rb +168 -0
  105. data/lib/datalog/plugins/comments.rb +33 -3
  106. data/lib/datalog/theme/installed_files.rb +113 -0
  107. data/lib/datalog/theme/repository_checkout.rb +6 -2
  108. data/lib/datalog/theme/version.rb +5 -1
  109. data/lib/datalog-theme.rb +1 -0
  110. metadata +57 -23
  111. data/assets/js/dist/chunks/chunk-225H5YXE.js +0 -1
@@ -1,23 +1,30 @@
1
1
  {%- comment -%}
2
2
  Author bio card with social links and credentials
3
3
  Parameters:
4
- - author: Author name (default: page.author or site.author.name)
4
+ - person: An author record from `page | page_authors` (default: the page's first author)
5
+ - author: An author's name or _data/authors.yml key, when no person is given
5
6
  - show_avatar: Show author avatar (default: true)
6
7
  - show_social: Show social links (default: true)
7
- - compact: Compact layout (default: false)
8
+ - compact: Avatar, name, affiliation and links only (default: false)
8
9
  {%- endcomment -%}
9
10
 
10
- {%- assign author_name = include.author | default: page.author | default: site.author.name -%}
11
11
  {%- assign show_avatar = include.show_avatar | default: true -%}
12
12
  {%- assign show_social = include.show_social | default: true -%}
13
13
  {%- assign compact = include.compact | default: false -%}
14
14
 
15
- {%- comment -%} Get author details from site.data.authors or site.author {%- endcomment -%}
16
- {%- if site.data.authors and site.data.authors[author_name] -%}
17
- {%- assign author_data = site.data.authors[author_name] -%}
15
+ {%- if include.person -%}
16
+ {%- assign author_data = include.person -%}
17
+ {%- elsif include.author -%}
18
+ {%- if site.data.authors and site.data.authors[include.author] -%}
19
+ {%- assign author_data = site.data.authors[include.author] -%}
20
+ {%- else -%}
21
+ {%- assign author_data = site.author -%}
22
+ {%- endif -%}
18
23
  {%- else -%}
19
- {%- assign author_data = site.author -%}
24
+ {%- assign bio_authors = page | page_authors -%}
25
+ {%- assign author_data = bio_authors | first -%}
20
26
  {%- endif -%}
27
+ {%- assign author_name = author_data.name | default: include.author | default: site.author.name | escape -%}
21
28
 
22
29
  <aside class="author-bio{% if compact %} author-bio--compact{% endif %}" itemscope itemtype="https://schema.org/Person">
23
30
  <div class="author-bio__inner">
@@ -33,7 +40,10 @@ Parameters:
33
40
  height="80">
34
41
  {%- else -%}
35
42
  {%- comment -%} Fallback avatar with initials {%- endcomment -%}
36
- {%- assign initials = author_name | split: ' ' | map: 'first' | slice: 0, 2 | join: '' | upcase -%}
43
+ {%- comment -%} `map: 'first'` on a list of words returned nothing, so the placeholder showed no initials. {%- endcomment -%}
44
+ {%- assign name_words = author_data.name | default: author_name | split: ' ' -%}
45
+ {%- capture initials -%}{%- for word in name_words limit: 2 -%}{{ word | slice: 0 }}{%- endfor -%}{%- endcapture -%}
46
+ {%- assign initials = initials | upcase | escape -%}
37
47
  <div class="author-bio__avatar-placeholder" role="img" aria-label="{{ author_name }}">
38
48
  <span aria-hidden="true">{{ initials }}</span>
39
49
  </div>
@@ -46,17 +56,17 @@ Parameters:
46
56
 
47
57
  {%- if author_data.affiliation -%}
48
58
  <p class="author-bio__affiliation" itemprop="affiliation">
49
- {{ author_data.affiliation }}
59
+ {{ author_data.affiliation | escape }}
50
60
  </p>
51
61
  {%- endif -%}
52
62
 
53
- {%- if author_data.biography or author_data.bio -%}
63
+ {%- if compact != true and author_data.biography or compact != true and author_data.bio -%}
54
64
  <p class="author-bio__description" itemprop="description">
55
65
  {{ author_data.biography | default: author_data.bio }}
56
66
  </p>
57
67
  {%- endif -%}
58
68
 
59
- {%- if author_data.research_areas and author_data.research_areas.size > 0 -%}
69
+ {%- if compact != true and author_data.research_areas and author_data.research_areas.size > 0 -%}
60
70
  <div class="author-bio__research">
61
71
  <strong class="author-bio__research-label">Research interests:</strong>
62
72
  <span class="author-bio__research-list">{{ author_data.research_areas | join: ', ' }}</span>
@@ -0,0 +1,32 @@
1
+ {%- comment -%}
2
+ ---
3
+ required:
4
+ - people
5
+ optional:
6
+ - property
7
+ - show_affiliation
8
+ ---
9
+ Lists the people who wrote or contributed to a page as schema.org Person
10
+ items: each name, linked to the person's url when there is one, with their
11
+ role and affiliation. `people` comes from `page | page_authors` or
12
+ `page | page_contributors`; `property` is the microdata property, author
13
+ (the default) or contributor.
14
+ {%- endcomment -%}
15
+ {%- assign list_property = include.property | default: 'author' -%}
16
+ <ul class="author-list">
17
+ {%- for person in include.people -%}
18
+ <li class="author-list__person" itemprop="{{ list_property }}" itemscope itemtype="https://schema.org/Person">
19
+ {%- if person.url -%}
20
+ {%- assign person_href = person.url -%}
21
+ {%- unless person_href contains '://' -%}{%- assign person_href = person_href | relative_url -%}{%- endunless -%}
22
+ <a href="{{ person_href | escape }}" itemprop="url"><span itemprop="name">{{ person.name | escape }}</span></a>
23
+ {%- else -%}
24
+ <span itemprop="name">{{ person.name | escape }}</span>
25
+ {%- endif -%}
26
+ {%- if person.role %} <span class="author-list__role">({{ person.role | escape }})</span>{%- endif -%}
27
+ {%- if person.affiliation and include.show_affiliation != false -%}
28
+ <span class="post-author-affiliation" itemprop="affiliation">{{ person.affiliation | escape }}</span>
29
+ {%- endif -%}
30
+ </li>
31
+ {%- endfor -%}
32
+ </ul>
@@ -2,27 +2,36 @@
2
2
  {% assign citation_url = include.url | default: page.url | default: '/' | absolute_url %}
3
3
  {% assign citation_date = include.date | default: page.date %}
4
4
  {% assign citation_year = citation_date | date: '%Y' %}
5
- {% assign authors_source = include.authors | default: page.authors | default: page.author | default: site.author.name %}
6
- {% assign authors_array = authors_source | arrayify %}
7
- {% assign publisher = include.publisher | default: site.author.affiliation | default: site.title %}
5
+ {%- comment -%} `authors`: records from `page | page_authors`, or names. Without it, the page's authors. {%- endcomment -%}
6
+ {% if include.authors %}
7
+ {% assign authors_array = include.authors %}
8
+ {% else %}
9
+ {% assign authors_array = page | page_authors %}
10
+ {% endif %}
11
+ {%- comment -%} A configured publisher, else the site's name. The author's affiliation is not the publisher. {%- endcomment -%}
12
+ {% assign publisher = include.publisher | default: site.publisher.name | default: site.title %}
8
13
  {% assign doi = include.doi | default: page.doi %}
9
14
  {% assign citation_type = include.citation_type | default: include.type | default: 'article' %}
10
15
  {% assign citation_key = include.citation_key | default: page.citation_key | default: page.slug | default: citation_title | slugify %}
11
- {% capture author_line %}
12
- {% for author in authors_array %}
13
- {% if author.name %}
14
- {{ author.name | strip }}{% unless forloop.last %} and {% endunless %}
15
- {% else %}
16
- {{ author | strip }}{% unless forloop.last %} and {% endunless %}
17
- {% endif %}
18
- {% endfor %}
19
- {% endcapture %}
20
- {% capture ris_authors %}
21
- {% for author in authors_array %}
22
- {% assign author_name = author.name | default: author %}
23
- AU - {{ author_name | strip }}
24
- {% endfor %}
25
- {% endcapture %}
16
+ {%- comment -%}
17
+ The author loops trim their whitespace: with several authors, the indentation
18
+ ended up inside the BibTeX author field and between the RIS author lines.
19
+ {%- endcomment -%}
20
+ {%- capture author_line -%}
21
+ {%- for author in authors_array -%}
22
+ {{- author.name | default: author | strip | escape -}}{%- unless forloop.last %} and {% endunless -%}
23
+ {%- endfor -%}
24
+ {%- endcapture -%}
25
+ {%- capture ris_authors -%}
26
+ {%- for author in authors_array %}
27
+ AU - {{ author.name | default: author | strip | escape }}
28
+ {%- endfor -%}
29
+ {%- endcapture -%}
30
+ {%- capture endnote_authors -%}
31
+ {%- for author in authors_array %}
32
+ %A {{ author.name | default: author | strip | escape }}
33
+ {%- endfor -%}
34
+ {%- endcapture -%}
26
35
  {% capture bibtex_entry %}@{{ citation_type }}{ {{ citation_key }},
27
36
  title = { {{ citation_title | strip }} },
28
37
  author = { {{ author_line | strip }} },
@@ -44,7 +53,7 @@ ER -
44
53
  {% endcapture %}
45
54
  {% capture endnote_entry %}%0 {{ citation_type | capitalize }}
46
55
  %T {{ citation_title | strip }}
47
- {{ ris_authors | strip | replace: 'AU', '%A' }}
56
+ {{ endnote_authors | strip }}
48
57
  {% if citation_year %}%D {{ citation_year }}
49
58
  {% endif %}
50
59
  {% if publisher %}%I {{ publisher | strip }}
@@ -53,12 +62,11 @@ ER -
53
62
  {% endif %}
54
63
  %U {{ citation_url }}
55
64
  {% endcapture %}
56
- {% capture authors_plain %}
57
- {% for author in authors_array %}
58
- {% assign author_name = author.name | default: author %}
59
- {{ author_name | strip }}{% unless forloop.last %}, {% endunless %}
60
- {% endfor %}
61
- {% endcapture %}
65
+ {%- capture authors_plain -%}
66
+ {%- for author in authors_array -%}
67
+ {{- author.name | default: author | strip | escape -}}{%- unless forloop.last -%}, {% endunless -%}
68
+ {%- endfor -%}
69
+ {%- endcapture -%}
62
70
  {% capture citation_text %}
63
71
  {{ authors_plain | strip }}{% if citation_year %} ({{ citation_year }}){% endif %}. {{ citation_title | strip }}. {{ publisher | strip }}. {{ citation_url }}.
64
72
  {% endcapture %}
@@ -0,0 +1,90 @@
1
+ {%- comment -%}
2
+ ---
3
+ optional: []
4
+ ---
5
+ The comments thread of the `api` provider (lib/datalog/plugins/comments.rb
6
+ renders it through the datalog_comments tag; assets/js/comments/thread.js
7
+ fills it): the discussion under an article, read from and written to the
8
+ site's own backend through the dynamic services (docs/dynamic-services.md).
9
+ Reads `page.datalog_comments` (the plugin's resolved settings: `replies`,
10
+ `moderation`, `endpoint`) and `page.url`, which keys the thread.
11
+ {%- endcomment -%}
12
+ {%- assign settings = page.datalog_comments -%}
13
+ {%- capture labels_json -%}
14
+ {
15
+ "loading": {{ 'comments.loading' | t | jsonify }},
16
+ "empty": {{ 'comments.empty' | t | jsonify }},
17
+ "retry": {{ 'comments.retry' | t | jsonify }},
18
+ "reply": {{ 'comments.reply' | t | jsonify }},
19
+ "replying_to": {{ 'comments.replying_to' | t | jsonify }},
20
+ "anonymous": {{ 'comments.anonymous' | t | jsonify }},
21
+ "pending": {{ 'comments.pending' | t | jsonify }},
22
+ "sending": {{ 'comments.sending' | t | jsonify }},
23
+ "posted": {{ 'comments.posted' | t | jsonify }},
24
+ "posted_pending": {{ 'comments.posted_pending' | t | jsonify }},
25
+ "name_required": {{ 'comments.name_required' | t | jsonify }},
26
+ "email_invalid": {{ 'comments.email_invalid' | t | jsonify }},
27
+ "url_invalid": {{ 'comments.url_invalid' | t | jsonify }},
28
+ "body_short": {{ 'comments.body_short' | t | jsonify }},
29
+ "errors": {
30
+ {%- assign kinds = "disabled,unsupported,version,timeout,network,malformed,unauthorized,forbidden,not_found,conflict,invalid,rate_limited,server,http,aborted,reference" | split: "," -%}
31
+ {%- for kind in kinds -%}
32
+ {%- assign kind_key = "dynamic_services.errors." | append: kind -%}
33
+ {{ kind | jsonify }}: {{ kind_key | t | jsonify }}{% unless forloop.last %},{% endunless %}
34
+ {%- endfor -%}
35
+ }
36
+ }
37
+ {%- endcapture -%}
38
+ <div class="comments-thread" id="comments" data-comments-thread data-comments-path="{{ page.url }}" data-comments-replies="{% if settings.replies == false %}false{% else %}true{% endif %}"{% if settings.endpoint and settings.endpoint != '' %} data-comments-endpoint="{{ settings.endpoint }}"{% endif %} data-state="idle">
39
+ <script type="application/json" nonce="{{ page.csp_nonce }}" data-comments-labels>{{ labels_json | strip }}</script>
40
+ <p class="comments-thread__status" data-comments-status role="status" aria-live="polite">{% t 'comments.loading' %}</p>
41
+ <noscript><p class="comments-thread__noscript">{% t 'comments.noscript' %}</p></noscript>
42
+ <ol class="comments-thread__list" data-comments-list hidden></ol>
43
+ <form class="service-form comments-form" id="comments-form" data-comments-form data-state="idle" aria-busy="false" aria-labelledby="comments-form-heading" novalidate>
44
+ <h3 class="comments-form__heading" id="comments-form-heading">{% t 'comments.form_heading' %}</h3>
45
+ <p class="comments-form__replying" data-comments-replying hidden>
46
+ <span data-comments-replying-to></span>
47
+ <button type="button" class="post-tool" data-comments-cancel-reply>{% t 'comments.cancel_reply' %}</button>
48
+ </p>
49
+ <fieldset class="service-form__fields">
50
+ <legend class="visually-hidden">{% t 'comments.form_heading' %}</legend>
51
+ <div class="service-form__row">
52
+ <div class="service-form__field">
53
+ <label for="comments-name">{% t 'comments.name' %}</label>
54
+ <input id="comments-name" name="name" type="text" required autocomplete="name">
55
+ <p class="service-form__error" data-error-for="name" hidden></p>
56
+ </div>
57
+ <div class="service-form__field">
58
+ <label for="comments-email">{% t 'comments.email' %}</label>
59
+ <input id="comments-email" name="email" type="email" autocomplete="email" aria-describedby="comments-email-hint">
60
+ <p class="service-form__hint" id="comments-email-hint">{% t 'comments.email_hint' %}</p>
61
+ <p class="service-form__error" data-error-for="email" hidden></p>
62
+ </div>
63
+ </div>
64
+ <div class="service-form__field">
65
+ <label for="comments-url">{% t 'comments.url' %}</label>
66
+ <input id="comments-url" name="url" type="url" autocomplete="url">
67
+ <p class="service-form__error" data-error-for="url" hidden></p>
68
+ </div>
69
+ <div class="service-form__field">
70
+ <label for="comments-body">{% t 'comments.body' %}</label>
71
+ <textarea id="comments-body" name="body" rows="5" required></textarea>
72
+ <p class="service-form__error" data-error-for="body" hidden></p>
73
+ </div>
74
+ <input type="hidden" name="parent_id" value="">
75
+ <div class="service-form__trap" aria-hidden="true">
76
+ <label for="comments-website">Website</label>
77
+ <input id="comments-website" name="website" type="text" tabindex="-1" autocomplete="off">
78
+ </div>
79
+ </fieldset>
80
+ {%- if settings.moderation != false %}
81
+ <p class="service-form__privacy">{% t 'comments.moderated' %}</p>
82
+ {%- else %}
83
+ <p class="service-form__privacy">{% t 'comments.privacy' %}</p>
84
+ {%- endif %}
85
+ <div class="service-form__actions">
86
+ <button type="submit" class="post-tool">{% t 'comments.send' %}</button>
87
+ </div>
88
+ <p class="service-form__status" data-form-status role="status" aria-live="polite" hidden></p>
89
+ </form>
90
+ </div>
@@ -0,0 +1,120 @@
1
+ {%- comment -%}
2
+ ---
3
+ optional: []
4
+ ---
5
+ The contact and collaboration form (assets/js/contact/form.js): a structured
6
+ message to the author, research collaboration, consulting, speaking,
7
+ mentoring, a reproducibility question, a media request or anything else,
8
+ sent to the site's dynamic services (docs/dynamic-services.md). Rendered by
9
+ the page layout when the page says `contact_form: true`, or included
10
+ anywhere. Without a backend offering the feature it falls back to the
11
+ site's email address, so the page is never empty. Categories come from
12
+ `contact.categories`, their labels and prompts from `contact.categories.*`
13
+ and `contact.prompts.*` in _data/i18n; `contact.prompts` in _config.yml
14
+ overrides a prompt.
15
+ {%- endcomment -%}
16
+ {%- assign services = site.dynamic_services -%}
17
+ {%- assign contact = site.contact -%}
18
+ {%- if contact.enabled != false -%}
19
+ {%- assign mail = site.contact_email | default: site.author.email -%}
20
+ {%- if services.base_url and services.base_url != '' and services.features.contact != false -%}
21
+ {%- assign categories = contact.categories -%}
22
+ {%- if categories == nil or categories.size == 0 -%}
23
+ {%- assign categories = "research-collaboration,consulting,speaking,mentoring,reproducibility,media,other" | split: "," -%}
24
+ {%- endif -%}
25
+ {%- capture labels_json -%}
26
+ {
27
+ "pending": {{ 'contact.pending' | t | jsonify }},
28
+ "success": {{ 'contact.success' | t | jsonify }},
29
+ "name_required": {{ 'contact.name_required' | t | jsonify }},
30
+ "email_invalid": {{ 'contact.email_invalid' | t | jsonify }},
31
+ "subject_required": {{ 'contact.subject_required' | t | jsonify }},
32
+ "message_short": {{ 'contact.message_short' | t | jsonify }},
33
+ "errors": {
34
+ {%- assign kinds = "disabled,unsupported,version,timeout,network,malformed,unauthorized,forbidden,not_found,conflict,invalid,rate_limited,server,http,aborted,reference" | split: "," -%}
35
+ {%- for kind in kinds -%}
36
+ {%- assign kind_key = "dynamic_services.errors." | append: kind -%}
37
+ {{ kind | jsonify }}: {{ kind_key | t | jsonify }}{% unless forloop.last %},{% endunless %}
38
+ {%- endfor -%}
39
+ }
40
+ }
41
+ {%- endcapture -%}
42
+ {%- capture prompts_json -%}
43
+ {
44
+ {%- for category in categories -%}
45
+ {%- assign prompt = nil -%}
46
+ {%- if contact.prompts -%}{%- assign prompt = contact.prompts[category] -%}{%- endif -%}
47
+ {%- unless prompt -%}
48
+ {%- assign prompt_key = "contact.prompts." | append: category -%}
49
+ {%- assign prompt = prompt_key | t -%}
50
+ {%- if prompt == prompt_key -%}{%- assign prompt = nil -%}{%- endif -%}
51
+ {%- endunless -%}
52
+ {%- if prompt -%}{{ category | jsonify }}: {{ prompt | jsonify }},{%- endif -%}
53
+ {%- endfor -%}
54
+ "": ""
55
+ }
56
+ {%- endcapture -%}
57
+ {%- assign privacy = contact.privacy_notice | default: nil -%}
58
+ {%- unless privacy -%}{%- assign privacy = 'contact.privacy' | t -%}{%- endunless -%}
59
+ <section class="contact-form" id="contact" data-contact-form data-source-url="{{ page.url | absolute_url }}">
60
+ <script type="application/json" nonce="{{ page.csp_nonce }}" data-contact-labels>{{ labels_json | strip }}</script>
61
+ <script type="application/json" nonce="{{ page.csp_nonce }}" data-contact-prompts>{{ prompts_json | strip }}</script>
62
+ <form class="service-form contact-form__form" id="contact-form" data-state="idle" aria-busy="false" novalidate>
63
+ <p class="service-form__intro">{% t 'contact.intro' %}</p>
64
+ <fieldset class="service-form__fields">
65
+ <legend class="visually-hidden">{% t 'contact.legend' %}</legend>
66
+ <div class="service-form__field">
67
+ <label for="contact-category">{% t 'contact.category' %}</label>
68
+ <select id="contact-category" name="category">
69
+ {%- for category in categories %}
70
+ {%- assign category_key = "contact.categories." | append: category %}
71
+ <option value="{{ category | escape }}">{{ category_key | t }}</option>
72
+ {%- endfor %}
73
+ </select>
74
+ </div>
75
+ <div class="service-form__row">
76
+ <div class="service-form__field">
77
+ <label for="contact-name">{% t 'contact.name' %}</label>
78
+ <input id="contact-name" name="name" type="text" required autocomplete="name">
79
+ <p class="service-form__error" data-error-for="name" hidden></p>
80
+ </div>
81
+ <div class="service-form__field">
82
+ <label for="contact-email">{% t 'contact.email' %}</label>
83
+ <input id="contact-email" name="email" type="email" required autocomplete="email">
84
+ <p class="service-form__error" data-error-for="email" hidden></p>
85
+ </div>
86
+ </div>
87
+ <div class="service-form__field">
88
+ <label for="contact-affiliation">{% t 'contact.affiliation' %}</label>
89
+ <input id="contact-affiliation" name="affiliation" type="text" autocomplete="organization">
90
+ </div>
91
+ <div class="service-form__field">
92
+ <label for="contact-subject">{% t 'contact.subject' %}</label>
93
+ <input id="contact-subject" name="subject" type="text" required>
94
+ <p class="service-form__error" data-error-for="subject" hidden></p>
95
+ </div>
96
+ <div class="service-form__field">
97
+ <label for="contact-message">{% t 'contact.message' %}</label>
98
+ <textarea id="contact-message" name="message" rows="7" required minlength="20" aria-describedby="contact-message-hint"></textarea>
99
+ <p class="service-form__hint" id="contact-message-hint" data-contact-hint>{% t 'contact.message_hint' %}</p>
100
+ <p class="service-form__error" data-error-for="message" hidden></p>
101
+ </div>
102
+ <div class="service-form__trap" aria-hidden="true">
103
+ <label for="contact-website">Website</label>
104
+ <input id="contact-website" name="website" type="text" tabindex="-1" autocomplete="off">
105
+ </div>
106
+ </fieldset>
107
+ <p class="service-form__privacy">{{ privacy }}{% if contact.retention %} {{ contact.retention }}{% endif %}</p>
108
+ <div class="service-form__actions">
109
+ <button type="submit" class="post-tool">{% t 'contact.send' %}</button>
110
+ </div>
111
+ <p class="service-form__status" data-form-status role="status" aria-live="polite" hidden></p>
112
+ </form>
113
+ </section>
114
+ {%- elsif mail -%}
115
+ {%- capture mail_link -%}<a href="mailto:{{ mail }}">{{ mail }}</a>{%- endcapture -%}
116
+ <section class="contact-form contact-form--fallback" id="contact">
117
+ <p class="contact-form__fallback">{% t 'contact.fallback' email: mail_link %}</p>
118
+ </section>
119
+ {%- endif -%}
120
+ {%- endif -%}
@@ -0,0 +1,82 @@
1
+ {%- comment -%}
2
+ ---
3
+ optional: []
4
+ ---
5
+ "Report an error or suggest a correction" (assets/js/corrections/form.js):
6
+ structured feedback about an article, apart from comments, sent to the
7
+ site's dynamic services (docs/dynamic-services.md) and never shown on the
8
+ page. Renders only when the site has a backend with the corrections feature
9
+ on, `corrections.enabled` is not false, and the page does not say
10
+ `corrections: false`. The categories come from `corrections.categories`,
11
+ their labels from `corrections.categories.*` in _data/i18n.
12
+ {%- endcomment -%}
13
+ {%- assign services = site.dynamic_services -%}
14
+ {%- assign corrections = site.corrections -%}
15
+ {%- if services.base_url and services.base_url != '' and services.features.corrections != false and corrections.enabled != false and page.corrections != false -%}
16
+ {%- assign categories = corrections.categories -%}
17
+ {%- if categories == nil or categories.size == 0 -%}
18
+ {%- assign categories = "mathematical-error,factual-error,citation,code,reproducibility,typo,accessibility,other" | split: "," -%}
19
+ {%- endif -%}
20
+ {%- capture labels_json -%}
21
+ {
22
+ "pending": {{ 'corrections.pending' | t | jsonify }},
23
+ "success": {{ 'corrections.success' | t | jsonify }},
24
+ "message_short": {{ 'corrections.message_short' | t | jsonify }},
25
+ "email_invalid": {{ 'corrections.email_invalid' | t | jsonify }},
26
+ "errors": {
27
+ {%- assign kinds = "disabled,unsupported,version,timeout,network,malformed,unauthorized,forbidden,not_found,conflict,invalid,rate_limited,server,http,aborted,reference" | split: "," -%}
28
+ {%- for kind in kinds -%}
29
+ {%- assign kind_key = "dynamic_services.errors." | append: kind -%}
30
+ {{ kind | jsonify }}: {{ kind_key | t | jsonify }}{% unless forloop.last %},{% endunless %}
31
+ {%- endfor -%}
32
+ }
33
+ }
34
+ {%- endcapture -%}
35
+ <details class="correction-report" id="correction-report" data-correction-report data-article-url="{{ page.url | absolute_url }}" data-article-title="{{ page.title | escape }}">
36
+ <summary class="correction-report__summary">{% t 'corrections.summary' %}</summary>
37
+ <script type="application/json" nonce="{{ page.csp_nonce }}" data-correction-labels>{{ labels_json | strip }}</script>
38
+ <form class="service-form correction-report__form" id="correction-report-form" data-state="idle" aria-busy="false" novalidate>
39
+ <p class="service-form__intro">{% t 'corrections.intro' %}</p>
40
+ <p class="service-form__quote" data-correction-quote hidden></p>
41
+ <fieldset class="service-form__fields">
42
+ <legend class="visually-hidden">{% t 'corrections.summary' %}</legend>
43
+ <div class="service-form__field">
44
+ <label for="correction-category">{% t 'corrections.category' %}</label>
45
+ <select id="correction-category" name="category">
46
+ {%- for category in categories %}
47
+ {%- assign category_key = "corrections.categories." | append: category %}
48
+ <option value="{{ category | escape }}">{{ category_key | t }}</option>
49
+ {%- endfor %}
50
+ </select>
51
+ </div>
52
+ <div class="service-form__field">
53
+ <label for="correction-section">{% t 'corrections.section' %}</label>
54
+ <select id="correction-section" name="section">
55
+ <option value="">{% t 'corrections.section_any' %}</option>
56
+ </select>
57
+ </div>
58
+ <div class="service-form__field">
59
+ <label for="correction-message">{% t 'corrections.message' %}</label>
60
+ <textarea id="correction-message" name="message" rows="5" required minlength="20" aria-describedby="correction-message-hint"></textarea>
61
+ <p class="service-form__hint" id="correction-message-hint">{% t 'corrections.message_hint' %}</p>
62
+ <p class="service-form__error" data-error-for="message" hidden></p>
63
+ </div>
64
+ <div class="service-form__field">
65
+ <label for="correction-email">{% t 'corrections.contact_email' %}</label>
66
+ <input id="correction-email" name="contact_email" type="email" autocomplete="email" aria-describedby="correction-email-hint">
67
+ <p class="service-form__hint" id="correction-email-hint">{% t 'corrections.contact_hint' %}</p>
68
+ <p class="service-form__error" data-error-for="contact_email" hidden></p>
69
+ </div>
70
+ <input type="hidden" name="quote" value="">
71
+ <div class="service-form__trap" aria-hidden="true">
72
+ <label for="correction-website">Website</label>
73
+ <input id="correction-website" name="website" type="text" tabindex="-1" autocomplete="off">
74
+ </div>
75
+ </fieldset>
76
+ <div class="service-form__actions">
77
+ <button type="submit" class="post-tool">{% t 'corrections.send' %}</button>
78
+ </div>
79
+ <p class="service-form__status" data-form-status role="status" aria-live="polite" hidden></p>
80
+ </form>
81
+ </details>
82
+ {%- endif -%}
@@ -27,7 +27,8 @@ Parameters:
27
27
  {%- endcapture -%}
28
28
 
29
29
  {%- if toc_markup contains '<li' -%}
30
- <aside class="enhanced-toc{% if collapsible %} enhanced-toc--collapsible{% endif %}"
30
+ {%- comment -%} A <nav>: doc-toc refines the navigation landmark, and is not a role an <aside> may carry. {%- endcomment -%}
31
+ <nav class="enhanced-toc{% if collapsible %} enhanced-toc--collapsible{% endif %}"
31
32
  data-toc-enhanced
32
33
  aria-label="{{ toc_heading }}"
33
34
  role="doc-toc">
@@ -87,7 +88,7 @@ Parameters:
87
88
  <span data-progress-percent>0</span>% complete
88
89
  </span>
89
90
  </div>
90
- </aside>
91
+ </nav>
91
92
 
92
93
  <script>
93
94
  (function() {
@@ -154,7 +155,8 @@ Parameters:
154
155
  }
155
156
  }
156
157
 
157
- // Smooth scroll for TOC links
158
+ // Scroll to TOC targets. The stylesheet's scroll-behavior decides whether
159
+ // the scroll is smooth, so reduced-motion preferences are respected.
158
160
  tocLinks.forEach(link => {
159
161
  link.addEventListener('click', function(e) {
160
162
  const targetId = this.getAttribute('href');
@@ -163,10 +165,7 @@ Parameters:
163
165
  const targetElement = document.querySelector(targetId);
164
166
  if (targetElement) {
165
167
  const offsetTop = targetElement.offsetTop - 80; // Account for sticky header
166
- window.scrollTo({
167
- top: offsetTop,
168
- behavior: 'smooth'
169
- });
168
+ window.scrollTo({ top: offsetTop });
170
169
  // Update URL without jumping
171
170
  if (history.pushState) {
172
171
  history.pushState(null, null, targetId);
@@ -0,0 +1,13 @@
1
+ {%- comment -%}
2
+ ---
3
+ required:
4
+ - license
5
+ ---
6
+ A licence's name (from the page_license or page_code_license filter), linked
7
+ with rel="license" when the licence has a URL.
8
+ {%- endcomment -%}
9
+ {%- if include.license.url -%}
10
+ <a class="license-link" rel="license" href="{{ include.license.url | escape }}">{{ include.license.name | escape }}</a>
11
+ {%- else -%}
12
+ <span class="license-link">{{ include.license.name | escape }}</span>
13
+ {%- endif -%}
@@ -0,0 +1,28 @@
1
+ {%- comment -%}
2
+ ---
3
+ required:
4
+ - page
5
+ ---
6
+ The reuse terms of an article: its copyright holder and year, the licence of
7
+ its text and figures and, when it differs, of its code samples
8
+ (_plugins/licenses.rb). Renders nothing for a page without a licence.
9
+ {%- endcomment -%}
10
+ {%- assign content_license = include.page | page_license -%}
11
+ {%- assign code_license = include.page | page_code_license -%}
12
+ {%- if content_license or code_license -%}
13
+ {%- capture content_link -%}{% include components/license-link.html license=content_license %}{%- endcapture -%}
14
+ {%- capture code_link -%}{% include components/license-link.html license=code_license %}{%- endcapture -%}
15
+ {%- assign content_link = content_link | strip -%}
16
+ {%- assign code_link = code_link | strip -%}
17
+ <p class="content-license">
18
+ {%- if content_license and content_license.holder %}
19
+ <span class="content-license__holder">©{% if content_license.year %} {{ content_license.year }}{% endif %} {{ content_license.holder | escape }}.</span>
20
+ {%- endif %}
21
+ {%- if content_license %}
22
+ {% if content_license.reserved %}{% t 'post.license.reserved' %}{% else %}{% t 'post.license.content' license: content_link %}{% endif %}
23
+ {%- endif %}
24
+ {%- if code_license %}
25
+ {% t 'post.license.code' license: code_link %}
26
+ {%- endif %}
27
+ </p>
28
+ {%- endif -%}