datalog-theme 0.8.0 → 0.9.1

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 (127) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +84 -0
  3. data/CITATION.cff +2 -2
  4. data/README.md +4 -2
  5. data/_data/i18n/en.yml +321 -0
  6. data/_data/i18n/es.yml +321 -0
  7. data/_data/i18n/pt.yml +321 -0
  8. data/_data/js_manifest.json +16 -0
  9. data/_includes/components/academic-dashboard.html +4 -4
  10. data/_includes/components/author-bio.html +21 -11
  11. data/_includes/components/author-list.html +32 -0
  12. data/_includes/components/breadcrumbs.html +6 -4
  13. data/_includes/components/citation-tools.html +18 -58
  14. data/_includes/components/comments-thread.html +90 -0
  15. data/_includes/components/contact-form.html +120 -0
  16. data/_includes/components/correction-report.html +82 -0
  17. data/_includes/components/enhanced-toc.html +6 -7
  18. data/_includes/components/hero.html +1 -1
  19. data/_includes/components/license-link.html +13 -0
  20. data/_includes/components/license-notice.html +28 -0
  21. data/_includes/components/moderation-inbox.html +139 -0
  22. data/_includes/components/post-hero.html +2 -2
  23. data/_includes/components/reactions.html +49 -0
  24. data/_includes/components/reading-list.html +34 -0
  25. data/_includes/components/reading-mode-toggle.html +67 -0
  26. data/_includes/components/reading-state-bookmark.html +27 -0
  27. data/_includes/components/reading-state-panel.html +62 -0
  28. data/_includes/components/reading-state-resume.html +17 -0
  29. data/_includes/components/reproducibility.html +61 -0
  30. data/_includes/components/responsive-image.html +3 -3
  31. data/_includes/components/revision-history.html +39 -0
  32. data/_includes/components/revision-notice.html +35 -0
  33. data/_includes/components/series-nav.html +64 -0
  34. data/_includes/components/subscribe-form.html +88 -0
  35. data/_includes/components/subscription-manage.html +69 -0
  36. data/_includes/components/visualization-card.html +1 -1
  37. data/_includes/components/webmentions.html +48 -0
  38. data/_includes/csp-meta.html +21 -1
  39. data/_includes/footer/nav-column.html +1 -1
  40. data/_includes/footer.html +5 -2
  41. data/_includes/head.html +64 -52
  42. data/_includes/header/navigation.html +2 -2
  43. data/_includes/header.html +1 -1
  44. data/_includes/layouts/default/article.html +6 -8
  45. data/_includes/meta/dynamic-services-config.html +14 -0
  46. data/_includes/meta/math-config.html +12 -5
  47. data/_includes/meta/person-json.html +27 -0
  48. data/_includes/meta/publisher.html +45 -0
  49. data/_includes/meta/schema.html +84 -48
  50. data/_includes/meta/scholarly.html +122 -0
  51. data/_includes/meta/scripts-loader.html +2 -0
  52. data/_includes/meta/webmention-discovery.html +14 -0
  53. data/_includes/post/related-posts.html +1 -1
  54. data/_includes/scripts.html +57 -0
  55. data/_layouts/dataset.html +5 -4
  56. data/_layouts/default.html +1 -1
  57. data/_layouts/home.html +2 -2
  58. data/_layouts/notebook.html +1 -1
  59. data/_layouts/package.html +8 -7
  60. data/_layouts/page.html +15 -0
  61. data/_layouts/portfolio.html +1 -1
  62. data/_layouts/post.html +94 -25
  63. data/_layouts/project.html +5 -5
  64. data/_layouts/research.html +27 -75
  65. data/_plugins/authors.rb +151 -0
  66. data/_plugins/citation_exports.rb +93 -0
  67. data/_plugins/config_validator.rb +223 -20
  68. data/_plugins/critical_css_check.rb +42 -0
  69. data/_plugins/i18n.rb +6 -4
  70. data/_plugins/image_optimizer.rb +240 -186
  71. data/_plugins/licenses.rb +147 -0
  72. data/_plugins/math_preprocessor.rb +55 -11
  73. data/_plugins/references.rb +260 -0
  74. data/_plugins/reproducibility.rb +167 -0
  75. data/_plugins/responsive_content.rb +40 -0
  76. data/_plugins/revisions.rb +102 -0
  77. data/_plugins/scholarly.rb +55 -0
  78. data/_plugins/series.rb +104 -0
  79. data/_plugins/statements.rb +87 -0
  80. data/_sass/_base.scss +13 -1
  81. data/_sass/_comments-thread.scss +159 -0
  82. data/_sass/_layout.scss +402 -1
  83. data/_sass/_mathematical.scss +38 -1
  84. data/_sass/_moderation.scss +222 -0
  85. data/_sass/_post-components.scss +4 -2
  86. data/_sass/_print.scss +299 -0
  87. data/_sass/_reactions.scss +89 -0
  88. data/_sass/_reading-state.scss +290 -0
  89. data/_sass/_service-forms.scss +204 -0
  90. data/_sass/_subscriptions.scss +140 -0
  91. data/_sass/_syntax-highlighting.scss +3 -3
  92. data/_sass/_theme.scss +11 -0
  93. data/_sass/_typography.scss +116 -0
  94. data/_sass/_utilities.scss +5 -0
  95. data/_sass/_variables.scss +6 -0
  96. data/_sass/_webmentions.scss +125 -0
  97. data/assets/js/dist/academic.js +1 -1
  98. data/assets/js/dist/analytics-dashboard.js +1 -1
  99. data/assets/js/dist/chunks/chunk-PATLC23F.js +1 -0
  100. data/assets/js/dist/chunks/chunk-VZ5WKQVA.js +1 -0
  101. data/assets/js/dist/chunks/chunk-WIRUK3OZ.js +1 -0
  102. data/assets/js/dist/comments.js +2 -0
  103. data/assets/js/dist/contact.js +1 -0
  104. data/assets/js/dist/core.js +1 -1
  105. data/assets/js/dist/corrections.js +1 -0
  106. data/assets/js/dist/loader.js +1 -1
  107. data/assets/js/dist/math.js +1 -1
  108. data/assets/js/dist/moderation.js +1 -0
  109. data/assets/js/dist/notebook.js +1 -1
  110. data/assets/js/dist/reactions.js +1 -0
  111. data/assets/js/dist/reading-state.js +1 -0
  112. data/assets/js/dist/search.js +1 -1
  113. data/assets/js/dist/sources.json +52 -0
  114. data/assets/js/dist/subscriptions.js +1 -0
  115. data/assets/js/dist/visualizations.js +2 -2
  116. data/assets/js/dist/webmentions.js +1 -0
  117. data/assets/js/loader.js +34 -0
  118. data/datalog-theme.gemspec +1 -2
  119. data/lib/datalog/cli.rb +9 -1
  120. data/lib/datalog/critical_css.rb +168 -0
  121. data/lib/datalog/plugins/comments.rb +33 -3
  122. data/lib/datalog/theme/installed_files.rb +113 -0
  123. data/lib/datalog/theme/repository_checkout.rb +6 -2
  124. data/lib/datalog/theme/version.rb +5 -1
  125. data/lib/datalog-theme.rb +1 -0
  126. metadata +60 -23
  127. data/assets/js/dist/chunks/chunk-225H5YXE.js +0 -1
@@ -0,0 +1,35 @@
1
+ {%- comment -%}
2
+ ---
3
+ required:
4
+ - page
5
+ optional:
6
+ - date_format
7
+ ---
8
+ The newest correction or update of an article (`revision_notice`, set from
9
+ `revisions` by _plugins/revisions.rb), announced under the metadata with a
10
+ link to the full history. Renders nothing without one, or with
11
+ `revision_notice: false` in front matter.
12
+ {%- endcomment -%}
13
+ {%- assign revision = include.page.revision_notice -%}
14
+ {%- if revision -%}
15
+ {%- capture revision_date -%}{% include helpers/date-format.html date=revision.date format=include.date_format %}{%- endcapture -%}
16
+ {%- assign revision_date = revision_date | strip -%}
17
+ <aside class="revision-notice revision-notice--{{ revision.type }}" aria-labelledby="revision-notice-lead">
18
+ <p class="revision-notice__text">
19
+ <strong id="revision-notice-lead" class="revision-notice__lead">
20
+ {%- if revision.type == 'correction' -%}
21
+ {% t 'post.revisions.corrected_on' date: revision_date %}
22
+ {%- else -%}
23
+ {% t 'post.revisions.updated_on' date: revision_date %}
24
+ {%- endif -%}
25
+ </strong>
26
+ {{ revision.summary | escape }}
27
+ {%- if revision.details_url -%}
28
+ {%- assign details_url = revision.details_url -%}
29
+ {%- unless details_url contains '://' -%}{%- assign details_url = details_url | relative_url -%}{%- endunless %}
30
+ <a class="revision-notice__details" href="{{ details_url | escape }}">{% t 'post.revisions.details' %}</a>
31
+ {%- endif %}
32
+ <a class="revision-notice__history" href="#revision-history">{% t 'post.revisions.history_link' %}</a>
33
+ </p>
34
+ </aside>
35
+ {%- endif -%}
@@ -0,0 +1,64 @@
1
+ {%- comment -%}
2
+ ---
3
+ required:
4
+ - page
5
+ optional:
6
+ - compact
7
+ ---
8
+ Navigation within an article series (`series` in front matter, resolved by
9
+ _plugins/series.rb): the series title, "Part k of n", every part in order
10
+ with the current one marked, and the previous and next parts. With
11
+ `compact: true`, the previous and next parts alone, for the end of the
12
+ article. Renders nothing for a page that is not part of a series.
13
+ {%- endcomment -%}
14
+ {%- assign series = include.page.series -%}
15
+ {%- if series.id -%}
16
+ {%- capture part_of -%}{% t 'post.series.part_of' part: series.position total: series.count %}{%- endcapture -%}
17
+ {%- capture nav_label -%}{% t 'post.series.nav_label' title: series.title %}{%- endcapture -%}
18
+ {%- capture series_links -%}
19
+ {%- if series.previous or series.next %}
20
+ <div class="series-nav__links">
21
+ {%- if series.previous %}
22
+ <a class="series-nav__link series-nav__link--previous" rel="prev" href="{{ series.previous.url | relative_url }}">
23
+ <span class="series-nav__link-label">{% t 'post.series.previous' %}</span>
24
+ <span class="series-nav__link-title">{{ series.previous.title | escape }}</span>
25
+ </a>
26
+ {%- endif %}
27
+ {%- if series.next %}
28
+ <a class="series-nav__link series-nav__link--next" rel="next" href="{{ series.next.url | relative_url }}">
29
+ <span class="series-nav__link-label">{% t 'post.series.next' %}</span>
30
+ <span class="series-nav__link-title">{{ series.next.title | escape }}</span>
31
+ </a>
32
+ {%- endif %}
33
+ </div>
34
+ {%- endif %}
35
+ {%- endcapture -%}
36
+ {%- if include.compact -%}
37
+ {%- if series.previous or series.next -%}
38
+ <nav class="series-nav series-nav--compact" aria-label="{{ nav_label | strip | escape }}">
39
+ <p class="series-nav__context">{{ series.title | escape }} · {{ part_of | strip }}</p>
40
+ {{- series_links }}
41
+ </nav>
42
+ {%- endif -%}
43
+ {%- else -%}
44
+ <nav class="series-nav" aria-labelledby="series-nav-heading">
45
+ <p class="series-nav__heading" id="series-nav-heading">
46
+ <span class="series-nav__label">{% t 'post.series.label' %}</span>
47
+ <span class="series-nav__title">{{ series.title | escape }}</span>
48
+ <span class="series-nav__position">{{ part_of | strip }}</span>
49
+ </p>
50
+ {%- if series.description %}
51
+ <p class="series-nav__description">{{ series.description | escape }}</p>
52
+ {%- endif %}
53
+ <progress class="series-nav__progress" value="{{ series.position }}" max="{{ series.count }}" aria-hidden="true"></progress>
54
+ <ol class="series-nav__list">
55
+ {%- for part in series.parts %}
56
+ <li class="series-nav__part{% if part.current %} series-nav__part--current{% endif %}">
57
+ <a href="{{ part.url | relative_url }}"{% if part.current %} aria-current="page"{% endif %}>{{ part.title | escape }}</a>
58
+ </li>
59
+ {%- endfor %}
60
+ </ol>
61
+ {{- series_links }}
62
+ </nav>
63
+ {%- endif -%}
64
+ {%- endif -%}
@@ -0,0 +1,88 @@
1
+ {%- comment -%}
2
+ ---
3
+ optional:
4
+ - where
5
+ ---
6
+ The newsletter subscribe form (assets/js/subscriptions/form.js): an email
7
+ address and, when `subscriptions.topics` lists any, what to receive, sent
8
+ to the site's dynamic services (docs/dynamic-services.md). Renders only
9
+ when the site has a backend with the subscriptions feature on and
10
+ `subscriptions.enabled` is not false; the footer and the post layout
11
+ include it where `subscriptions.placement` says. `where` ("footer",
12
+ "post" or "page", the default) keeps the ids apart when a page carries
13
+ more than one, and picks the heading level.
14
+
15
+ The footer is an include_cached, rendered once per language and reused on
16
+ every page, so the footer form carries nothing of the page: the script
17
+ reads the page's address from its canonical link and its language from
18
+ <html lang>. For the same reason the block has no accessible name of its
19
+ own, which would make two same-named landmarks on a post.
20
+ {%- endcomment -%}
21
+ {%- assign services = site.dynamic_services -%}
22
+ {%- assign subscriptions = site.subscriptions -%}
23
+ {%- if services.base_url and services.base_url != '' and services.features.subscriptions != false and subscriptions.enabled != false -%}
24
+ {%- assign where = include.where | default: 'page' -%}
25
+ {%- assign topics = subscriptions.topics -%}
26
+ {%- assign privacy_url = subscriptions.privacy_url | default: nil -%}
27
+ {%- capture labels_json -%}
28
+ {
29
+ "sending": {{ 'subscriptions.sending' | t | jsonify }},
30
+ "pending": {{ 'subscriptions.pending' | t | jsonify }},
31
+ "confirmed": {{ 'subscriptions.confirmed' | t | jsonify }},
32
+ "duplicate": {{ 'subscriptions.duplicate' | t | jsonify }},
33
+ "email_invalid": {{ 'subscriptions.email_invalid' | t | jsonify }},
34
+ "topics_required": {{ 'subscriptions.topics_required' | t | jsonify }},
35
+ "errors": {
36
+ {%- assign kinds = "disabled,unsupported,version,timeout,network,malformed,unauthorized,forbidden,not_found,conflict,invalid,rate_limited,server,http,aborted,retry_after,reference" | split: "," -%}
37
+ {%- for kind in kinds -%}
38
+ {%- assign kind_key = "dynamic_services.errors." | append: kind -%}
39
+ {{ kind | jsonify }}: {{ kind_key | t | jsonify }}{% unless forloop.last %},{% endunless %}
40
+ {%- endfor -%}
41
+ }
42
+ }
43
+ {%- endcapture -%}
44
+ <section class="subscribe subscribe--{{ where }}" data-subscribe{% unless where == 'footer' %} data-subscribe-source="{{ page.url | absolute_url }}" data-subscribe-locale="{{ page.lang | default: site.lang | default: 'en' }}"{% endunless %} data-subscribe-double-opt-in="{% if subscriptions.double_opt_in == false %}false{% else %}true{% endif %}">
45
+ <script type="application/json" data-subscribe-labels>{{ labels_json | strip }}</script>
46
+ {%- if where == 'footer' %}
47
+ <h3 class="subscribe__heading" id="subscribe-heading-{{ where }}">{% t 'footer.subscribe_title' %}</h3>
48
+ {%- else %}
49
+ <h2 class="subscribe__heading" id="subscribe-heading-{{ where }}">{% t 'footer.subscribe_title' %}</h2>
50
+ {%- endif %}
51
+ <p class="subscribe__description">{% t 'footer.subscribe_description' %}</p>
52
+ <form class="service-form subscribe__form" id="subscribe-form-{{ where }}" data-state="idle" aria-busy="false" novalidate>
53
+ <fieldset class="service-form__fields">
54
+ <legend class="visually-hidden">{% t 'footer.subscribe_title' %}</legend>
55
+ <div class="service-form__field">
56
+ <label for="subscribe-email-{{ where }}">{% t 'footer.newsletter_label' %}</label>
57
+ <input id="subscribe-email-{{ where }}" name="email" type="email" required autocomplete="email">
58
+ <p class="service-form__error" data-error-for="email" hidden></p>
59
+ </div>
60
+ {%- if topics and topics.size > 0 %}
61
+ <fieldset class="subscribe__topics">
62
+ <legend>{% t 'subscriptions.topics_legend' %}</legend>
63
+ {%- for topic in topics %}
64
+ {%- assign topic_key = "subscriptions.topics." | append: topic %}
65
+ <label class="subscribe__topic" for="subscribe-topic-{{ where }}-{{ topic | slugify }}">
66
+ <input id="subscribe-topic-{{ where }}-{{ topic | slugify }}" type="checkbox" name="topics" value="{{ topic | escape }}" checked>
67
+ {{ topic_key | t }}
68
+ </label>
69
+ {%- endfor %}
70
+ <p class="service-form__error" data-error-for="topics" hidden></p>
71
+ </fieldset>
72
+ {%- endif %}
73
+ <div class="service-form__trap" aria-hidden="true">
74
+ <label for="subscribe-website-{{ where }}">Website</label>
75
+ <input id="subscribe-website-{{ where }}" name="website" type="text" tabindex="-1" autocomplete="off">
76
+ </div>
77
+ </fieldset>
78
+ <p class="service-form__privacy">
79
+ {%- if subscriptions.double_opt_in == false %}{% t 'subscriptions.consent' %}{% else %}{% t 'subscriptions.consent_double' %}{% endif %}
80
+ {%- if privacy_url %} <a href="{{ privacy_url | relative_url }}">{% t 'subscriptions.privacy_link' %}</a>{% endif -%}
81
+ </p>
82
+ <div class="service-form__actions">
83
+ <button type="submit" class="post-tool">{% t 'footer.newsletter_button' %}</button>
84
+ </div>
85
+ <p class="service-form__status" data-form-status role="status" aria-live="polite" hidden></p>
86
+ </form>
87
+ </section>
88
+ {%- endif -%}
@@ -0,0 +1,69 @@
1
+ {%- comment -%}
2
+ ---
3
+ optional: []
4
+ ---
5
+ The page the newsletter's emails link to (assets/js/subscriptions/manage.js):
6
+ `?confirm=<token>` confirms a subscription, `?unsubscribe=<token>` ends one
7
+ after a press of the button, `?manage=<token>` changes its topics. Rendered
8
+ by the page layout when the page says `subscription_manage: true`. Without
9
+ a backend offering the feature it says so, since a link in an old email
10
+ may still lead here.
11
+ {%- endcomment -%}
12
+ {%- assign services = site.dynamic_services -%}
13
+ {%- assign subscriptions = site.subscriptions -%}
14
+ {%- if services.base_url and services.base_url != '' and services.features.subscriptions != false and subscriptions.enabled != false -%}
15
+ {%- assign topics = subscriptions.topics -%}
16
+ {%- capture labels_json -%}
17
+ {
18
+ "confirming": {{ 'subscriptions.manage.confirming' | t | jsonify }},
19
+ "confirmed": {{ 'subscriptions.manage.confirmed' | t | jsonify }},
20
+ "invalid_link": {{ 'subscriptions.manage.invalid_link' | t | jsonify }},
21
+ "unsubscribing": {{ 'subscriptions.manage.unsubscribing' | t | jsonify }},
22
+ "unsubscribed": {{ 'subscriptions.manage.unsubscribed' | t | jsonify }},
23
+ "loading": {{ 'subscriptions.manage.loading' | t | jsonify }},
24
+ "saving": {{ 'subscriptions.manage.saving' | t | jsonify }},
25
+ "saved": {{ 'subscriptions.manage.saved' | t | jsonify }},
26
+ "retry": {{ 'subscriptions.manage.retry' | t | jsonify }},
27
+ "errors": {
28
+ {%- assign kinds = "disabled,unsupported,version,timeout,network,malformed,unauthorized,forbidden,not_found,conflict,invalid,rate_limited,server,http,aborted,retry_after,reference" | split: "," -%}
29
+ {%- for kind in kinds -%}
30
+ {%- assign kind_key = "dynamic_services.errors." | append: kind -%}
31
+ {{ kind | jsonify }}: {{ kind_key | t | jsonify }}{% unless forloop.last %},{% endunless %}
32
+ {%- endfor -%}
33
+ }
34
+ }
35
+ {%- endcapture -%}
36
+ <section class="subscription-manage" id="subscription-manage" data-subscription-manage data-state="idle" aria-busy="false">
37
+ <script type="application/json" nonce="{{ page.csp_nonce }}" data-manage-labels>{{ labels_json | strip }}</script>
38
+ <p class="subscription-manage__status" data-manage-status role="status" aria-live="polite" hidden></p>
39
+ <p class="subscription-manage__idle" data-manage-idle>{% t 'subscriptions.manage.idle' %}</p>
40
+ <div class="subscription-manage__panel" data-manage-unsubscribe-panel hidden>
41
+ <p>{% t 'subscriptions.manage.unsubscribe_question' %}</p>
42
+ <button type="button" class="post-tool" data-manage-unsubscribe>{% t 'subscriptions.manage.unsubscribe' %}</button>
43
+ </div>
44
+ <form class="service-form subscription-manage__panel" id="subscription-preferences" data-manage-preferences hidden novalidate>
45
+ {%- if topics and topics.size > 0 %}
46
+ <fieldset class="subscribe__topics">
47
+ <legend>{% t 'subscriptions.manage.preferences' %}</legend>
48
+ {%- for topic in topics %}
49
+ {%- assign topic_key = "subscriptions.topics." | append: topic %}
50
+ <label class="subscribe__topic" for="manage-topic-{{ topic | slugify }}">
51
+ <input id="manage-topic-{{ topic | slugify }}" type="checkbox" name="topics" value="{{ topic | escape }}">
52
+ {{ topic_key | t }}
53
+ </label>
54
+ {%- endfor %}
55
+ </fieldset>
56
+ {%- endif %}
57
+ <div class="service-form__actions">
58
+ {%- if topics and topics.size > 0 %}
59
+ <button type="submit" class="post-tool">{% t 'subscriptions.manage.save' %}</button>
60
+ {%- endif %}
61
+ <button type="button" class="post-tool" data-manage-unsubscribe>{% t 'subscriptions.manage.unsubscribe' %}</button>
62
+ </div>
63
+ </form>
64
+ </section>
65
+ {%- else -%}
66
+ <section class="subscription-manage" id="subscription-manage">
67
+ <p class="subscription-manage__idle">{% t 'subscriptions.manage.unavailable' %}</p>
68
+ </section>
69
+ {%- endif -%}
@@ -4,7 +4,7 @@
4
4
  <span class="post-meta__badge">{{ tech }}</span>
5
5
  {% if visualization.version %}<span>v{{ visualization.version }}</span>{% endif %}
6
6
  </p>
7
- <h3>{{ visualization.title }}</h3>
7
+ <h3>{{ visualization.title | escape }}</h3>
8
8
  <p>{{ visualization.summary }}</p>
9
9
  {% if visualization.tags %}
10
10
  <ul class="post-list-item__tags">
@@ -0,0 +1,48 @@
1
+ {%- comment -%}
2
+ ---
3
+ optional: []
4
+ ---
5
+ "Mentioned elsewhere" (assets/js/webmentions/list.js): the links, replies
6
+ and reposts other websites sent about this article, read through the
7
+ site's dynamic services (docs/dynamic-services.md) from what the
8
+ Webmention receiver verified. Apart from the comments, which are hosted
9
+ here. Renders only when the site has a backend with the webmentions
10
+ feature on, `webmentions.enabled` is not false, and the page does not say
11
+ `webmentions: false`. The kinds shown come from `webmentions.types`.
12
+ The receiver itself is advertised by meta/webmention-discovery.html.
13
+ {%- endcomment -%}
14
+ {%- assign services = site.dynamic_services -%}
15
+ {%- assign webmentions = site.webmentions -%}
16
+ {%- if services.base_url and services.base_url != '' and services.features.webmentions != false and webmentions.enabled != false and page.webmentions != false -%}
17
+ {%- assign types = webmentions.types -%}
18
+ {%- if types == nil or types.size == 0 -%}
19
+ {%- assign types = "mention,reply" | split: "," -%}
20
+ {%- endif -%}
21
+ {%- capture labels_json -%}
22
+ {
23
+ "loading": {{ 'webmentions.loading' | t | jsonify }},
24
+ "empty": {{ 'webmentions.empty' | t | jsonify }},
25
+ "retry": {{ 'webmentions.retry' | t | jsonify }},
26
+ "types": {
27
+ "mention": {{ 'webmentions.types.mention' | t | jsonify }},
28
+ "reply": {{ 'webmentions.types.reply' | t | jsonify }},
29
+ "repost": {{ 'webmentions.types.repost' | t | jsonify }},
30
+ "like": {{ 'webmentions.types.like' | t | jsonify }}
31
+ },
32
+ "errors": {
33
+ {%- assign kinds = "disabled,unsupported,version,timeout,network,malformed,unauthorized,forbidden,not_found,conflict,invalid,rate_limited,server,http,aborted,retry_after,reference" | split: "," -%}
34
+ {%- for kind in kinds -%}
35
+ {%- assign kind_key = "dynamic_services.errors." | append: kind -%}
36
+ {{ kind | jsonify }}: {{ kind_key | t | jsonify }}{% unless forloop.last %},{% endunless %}
37
+ {%- endfor -%}
38
+ }
39
+ }
40
+ {%- endcapture -%}
41
+ <section class="webmentions" id="webmentions" data-webmentions data-webmentions-target="{{ page.url | absolute_url }}" data-webmentions-types="{{ types | join: ',' }}" data-state="idle" aria-labelledby="webmentions-heading">
42
+ <script type="application/json" nonce="{{ page.csp_nonce }}" data-webmentions-labels>{{ labels_json | strip }}</script>
43
+ <h2 class="webmentions__heading" id="webmentions-heading">{% t 'webmentions.heading' %}</h2>
44
+ <p class="webmentions__intro">{% t 'webmentions.intro' %}</p>
45
+ <p class="webmentions__status" data-webmentions-status role="status" aria-live="polite">{% t 'webmentions.loading' %}</p>
46
+ <ul class="webmentions__list" data-webmentions-list hidden></ul>
47
+ </section>
48
+ {%- endif -%}
@@ -10,6 +10,26 @@
10
10
  {% assign extra_style_src = site.csp.style_src | join: ' ' %}
11
11
  {% assign extra_font_src = site.csp.font_src | join: ' ' %}
12
12
  {% assign extra_connect_src = site.csp.connect_src | join: ' ' %}
13
+ {%- comment -%} The dynamic services' origin, so the browser may call the site's backend (docs/dynamic-services.md). {%- endcomment -%}
14
+ {% assign services_origin = '' %}
15
+ {% if site.dynamic_services.base_url and site.dynamic_services.base_url != '' %}
16
+ {% assign services_parts = site.dynamic_services.base_url | split: '/' %}
17
+ {% assign services_origin = services_parts[0] | append: '//' | append: services_parts[2] %}
18
+ {% endif %}
19
+ {%- comment -%} The comments endpoint, when the api provider keeps comments apart from the services (lib/datalog/plugins/comments.rb). {%- endcomment -%}
20
+ {% assign comments_origin = '' %}
21
+ {% assign comments_options = site.datalog_plugins.options['datalog-comments'] %}
22
+ {% assign comments_endpoint = page.datalog_comments.endpoint | default: comments_options.endpoint %}
23
+ {% if comments_endpoint and comments_endpoint != '' %}
24
+ {% assign comments_parts = comments_endpoint | split: '/' %}
25
+ {% assign comments_origin = comments_parts[0] | append: '//' | append: comments_parts[2] %}
26
+ {% endif %}
27
+ {%- comment -%} The moderation API, when it lives apart from the services, and only on the inbox page (docs/moderation.md). {%- endcomment -%}
28
+ {% assign moderation_origin = '' %}
29
+ {% if page.moderation_inbox and site.moderation.enabled == true and site.moderation.endpoint and site.moderation.endpoint != '' %}
30
+ {% assign moderation_parts = site.moderation.endpoint | split: '/' %}
31
+ {% assign moderation_origin = moderation_parts[0] | append: '//' | append: moderation_parts[2] %}
32
+ {% endif %}
13
33
  {%- comment -%}
14
34
  Plotly, the Jupyter widget manager and MathJax can't run under the strict
15
35
  policy. Plotly inserts its rules into a <style> element it creates; the widget
@@ -113,7 +133,7 @@
113
133
  style-src 'self' {% if csp_inline_styles %}'unsafe-inline'{% else %}'nonce-{{ nonce_source }}'{% endif %} https://fonts.googleapis.com{{ cdn_styles }}{% if extra_style_src != '' %} {{ extra_style_src }}{% endif %};
114
134
  img-src 'self' data: https:;
115
135
  font-src 'self' https://fonts.gstatic.com data:{{ cdn_fonts }}{% if extra_font_src != '' %} {{ extra_font_src }}{% endif %};
116
- connect-src 'self' https://api.github.com{{ cdn_connect }}{% if csp_analytics %} https://*.google-analytics.com https://*.analytics.google.com https://*.googletagmanager.com{% endif %}{% if extra_connect_src != '' %} {{ extra_connect_src }}{% endif %};
136
+ connect-src 'self' https://api.github.com{{ cdn_connect }}{% if csp_analytics %} https://*.google-analytics.com https://*.analytics.google.com https://*.googletagmanager.com{% endif %}{% if services_origin != '' %} {{ services_origin }}{% endif %}{% if comments_origin != '' and comments_origin != services_origin %} {{ comments_origin }}{% endif %}{% if moderation_origin != '' and moderation_origin != services_origin %} {{ moderation_origin }}{% endif %}{% if extra_connect_src != '' %} {{ extra_connect_src }}{% endif %};
117
137
  frame-src 'self' https://observablehq.com https://old.observablehq.com{{ cdn_frames }}{% if frame_hosts != '' %} {{ frame_hosts }}{% endif %};
118
138
  object-src 'none';
119
139
  base-uri 'self';
@@ -13,7 +13,7 @@ Renders a footer navigation column with optional item descriptions.
13
13
  {%- assign column_items = '' | split: '' -%}
14
14
  {%- endif -%}
15
15
  <div>
16
- <h4>{{ include.title | strip }}</h4>
16
+ <h4>{{ include.title | strip | escape }}</h4>
17
17
  <ul>
18
18
  {%- for item in column_items -%}
19
19
  {%- capture item_label -%}{% if item.label %}{{ item.label }}{% elsif item.label_key %}{% t item.label_key %}{% endif %}{%- endcapture -%}
@@ -6,7 +6,7 @@
6
6
  <footer class="site-footer" role="contentinfo">
7
7
  <div class="container footer-container">
8
8
  <section class="footer-about">
9
- <h2>{{ site.name | default: site.title }}</h2>
9
+ <h2>{{ site.name | default: site.title | escape }}</h2>
10
10
  <p class="footer-description">{{ site.description | strip_html | truncate: 160 }}</p>
11
11
  <p class="footer-author">
12
12
  <strong>{{ site.author.name }}</strong>
@@ -28,7 +28,7 @@
28
28
  <h2 id="footer-explore-heading">Explore</h2>
29
29
  <ul class="footer-link-list">
30
30
  {% for item in footer_links %}
31
- <li><a href="{{ item.url | relative_url }}">{{ item.title }}</a></li>
31
+ <li><a href="{{ item.url | relative_url }}">{{ item.title | escape }}</a></li>
32
32
  {% endfor %}
33
33
  </ul>
34
34
  </nav>
@@ -67,6 +67,9 @@
67
67
  <li><a href="{{ '/feed.xml' | relative_url }}">RSS feed</a></li>
68
68
  {% endif %}
69
69
  </ul>
70
+ {%- comment -%} The newsletter form, on a site with a backend that takes subscriptions {%- endcomment -%}
71
+ {% assign subscribe_placement = site.subscriptions.placement | default: 'footer' %}
72
+ {% if subscribe_placement contains 'footer' %}{% include components/subscribe-form.html where='footer' %}{% endif %}
70
73
  </section>
71
74
  </div>
72
75
  <div class="container footer-bottom">
data/_includes/head.html CHANGED
@@ -1,5 +1,6 @@
1
1
  {% assign page_title = page.seo_title | default: page.title | default: site.title %}
2
- {% assign page_description = page.seo_description | default: page.description | default: page.excerpt | default: site.description %}
2
+ {% assign excerpt_description = page.excerpt | strip_html %}
3
+ {% assign page_description = page.seo_description | default: page.description | default: excerpt_description | default: site.description %}
3
4
  {% assign canonical_url = page.url | default: page.permalink | default: '/' | absolute_url %}
4
5
  {% assign raw_keywords = page.keywords | default: page.tags | default: site.keywords %}
5
6
  {% assign localization_config = site.theme_options.localization %}
@@ -23,8 +24,9 @@
23
24
  {% include csp-meta.html %}
24
25
  <meta name="viewport" content="width=device-width, initial-scale=1" />
25
26
  <meta name="theme-color" content="#0b1d3d" />
26
- <meta name="description" content="{{ page_description | strip_html | normalize_whitespace }}" />
27
- <meta name="author" content="{{ page.author | default: site.author.name }}" />
27
+ <meta name="description" content="{{ page_description | normalize_whitespace | escape }}" />
28
+ {% assign head_authors = page | page_authors %}
29
+ <meta name="author" content="{{ head_authors | map: 'name' | join: ', ' | escape }}" />
28
30
  {% if keyword_list %}
29
31
  <meta name="keywords" content="{{ keyword_list | join: ', ' | strip }}" />
30
32
  {% endif %}
@@ -33,15 +35,15 @@
33
35
  {%- comment -%} `robots:` in front matter keeps a page out of search results, as the 404, search and admin pages do. {%- endcomment %}
34
36
  <meta name="robots" content="{{ page.robots | default: 'index,follow' }}" />
35
37
  <meta name="format-detection" content="telephone=no" />
36
- <meta name="application-name" content="{{ site.title }}" />
37
- <meta property="og:site_name" content="{{ site.title }}" />
38
+ <meta name="application-name" content="{{ site.title | escape }}" />
39
+ <meta property="og:site_name" content="{{ site.title | escape }}" />
38
40
  {% if page.layout == 'post' or page.collection == 'posts' %}
39
41
  <meta property="og:type" content="article" />
40
42
  {% else %}
41
43
  <meta property="og:type" content="website" />
42
44
  {% endif %}
43
- <meta property="og:title" content="{{ page_title | strip_html | normalize_whitespace }}" />
44
- <meta property="og:description" content="{{ page_description | strip_html | normalize_whitespace }}" />
45
+ <meta property="og:title" content="{{ page_title | normalize_whitespace | escape }}" />
46
+ <meta property="og:description" content="{{ page_description | normalize_whitespace | escape }}" />
45
47
  <meta property="og:url" content="{{ canonical_url }}" />
46
48
  {% if page.og_image or page.image %}
47
49
  <meta property="og:image" content="{{ page.og_image | default: page.image | absolute_url }}" />
@@ -52,14 +54,17 @@
52
54
  <meta property="og:see_also" content="{{ site.url }}" />
53
55
  <meta name="twitter:card" content="summary_large_image" />
54
56
  <meta name="twitter:site" content="{{ site.social.twitter | default: site.twitter_username }}" />
55
- <meta name="twitter:title" content="{{ page_title | strip_html | normalize_whitespace }}" />
56
- <meta name="twitter:description" content="{{ page_description | strip_html | normalize_whitespace }}" />
57
+ <meta name="twitter:title" content="{{ page_title | normalize_whitespace | escape }}" />
58
+ <meta name="twitter:description" content="{{ page_description | normalize_whitespace | escape }}" />
57
59
  {% if page.twitter_image or page.og_image or page.image %}
58
60
  <meta name="twitter:image" content="{{ page.twitter_image | default: page.og_image | default: page.image | absolute_url }}" />
59
61
  {% elsif site.social.default_image %}
60
62
  <meta name="twitter:image" content="{{ site.social.default_image | absolute_url }}" />
61
63
  {% endif %}
62
64
  <link rel="canonical" href="{{ canonical_url }}" />
65
+ {% include meta/webmention-discovery.html %}
66
+ {%- assign head_license = page | page_license %}
67
+ {% if head_license.url %}<link rel="license" href="{{ head_license.url | escape }}" />{% endif %}
63
68
  {% if js_manifest and js_manifest.core %}
64
69
  <link rel="modulepreload" href="{{ js_manifest.core | relative_url }}" crossorigin="anonymous" />
65
70
  {%- comment -%} Only a page that renders the search app uses the search bundle. {%- endcomment -%}
@@ -77,6 +82,50 @@
77
82
  {% if js_manifest.features and js_manifest.features.visualizations and site.features.visualizations and content contains 'data-viz-type' %}
78
83
  <link rel="modulepreload" href="{{ js_manifest.features.visualizations | relative_url }}" crossorigin="anonymous" />
79
84
  {% endif %}
85
+ {%- comment -%} Posts carry the reading-state panel; a page with the saved-articles list carries data-reading-list. {%- endcomment -%}
86
+ {% if js_manifest.features and js_manifest.features['reading-state'] and site.theme_options.reading_state.enabled != false %}
87
+ {% if page.layout == 'post' or content contains 'data-reading-list' %}
88
+ <link rel="modulepreload" href="{{ js_manifest.features['reading-state'] | relative_url }}" crossorigin="anonymous" />
89
+ {% endif %}
90
+ {% endif %}
91
+ {%- comment -%} The correction-report form renders on posts of a site with a backend. {%- endcomment -%}
92
+ {% if js_manifest.features and js_manifest.features.corrections and page.layout == 'post' and page.corrections != false %}
93
+ {% if site.dynamic_services.base_url and site.dynamic_services.base_url != '' and site.dynamic_services.features.corrections != false and site.corrections.enabled != false %}
94
+ <link rel="modulepreload" href="{{ js_manifest.features.corrections | relative_url }}" crossorigin="anonymous" />
95
+ {% endif %}
96
+ {% endif %}
97
+ {%- comment -%} The contact form renders on a page that asks for it, on a site with a backend. {%- endcomment -%}
98
+ {% if js_manifest.features and js_manifest.features.contact and page.contact_form %}
99
+ {% if site.dynamic_services.base_url and site.dynamic_services.base_url != '' and site.dynamic_services.features.contact != false and site.contact.enabled != false %}
100
+ <link rel="modulepreload" href="{{ js_manifest.features.contact | relative_url }}" crossorigin="anonymous" />
101
+ {% endif %}
102
+ {% endif %}
103
+ {%- comment -%} The comments thread of the api provider, on a page that shows comments that way. {%- endcomment -%}
104
+ {% if js_manifest.features and js_manifest.features.comments and page.datalog_comments.provider == 'api' %}
105
+ <link rel="modulepreload" href="{{ js_manifest.features.comments | relative_url }}" crossorigin="anonymous" />
106
+ {% endif %}
107
+ {%- comment -%} "Was this useful?" renders on posts of a site with a backend. {%- endcomment -%}
108
+ {% if js_manifest.features and js_manifest.features.reactions and page.layout == 'post' and page.reactions != false %}
109
+ {% if site.dynamic_services.base_url and site.dynamic_services.base_url != '' and site.dynamic_services.features.reactions != false and site.reactions.enabled != false %}
110
+ <link rel="modulepreload" href="{{ js_manifest.features.reactions | relative_url }}" crossorigin="anonymous" />
111
+ {% endif %}
112
+ {% endif %}
113
+ {%- comment -%} "Mentioned elsewhere" renders on posts of a site with a backend. {%- endcomment -%}
114
+ {% if js_manifest.features and js_manifest.features.webmentions and page.layout == 'post' and page.webmentions != false %}
115
+ {% if site.dynamic_services.base_url and site.dynamic_services.base_url != '' and site.dynamic_services.features.webmentions != false and site.webmentions.enabled != false %}
116
+ <link rel="modulepreload" href="{{ js_manifest.features.webmentions | relative_url }}" crossorigin="anonymous" />
117
+ {% endif %}
118
+ {% endif %}
119
+ {%- comment -%} The page the newsletter's emails link to acts as soon as it loads; the footer form loads on demand. {%- endcomment -%}
120
+ {% if js_manifest.features and js_manifest.features.subscriptions and page.subscription_manage %}
121
+ {% if site.dynamic_services.base_url and site.dynamic_services.base_url != '' and site.dynamic_services.features.subscriptions != false and site.subscriptions.enabled != false %}
122
+ <link rel="modulepreload" href="{{ js_manifest.features.subscriptions | relative_url }}" crossorigin="anonymous" />
123
+ {% endif %}
124
+ {% endif %}
125
+ {%- comment -%} The moderation inbox loads its queue as soon as the page opens. {%- endcomment -%}
126
+ {% if js_manifest.features and js_manifest.features.moderation and page.moderation_inbox and site.moderation.enabled == true %}
127
+ <link rel="modulepreload" href="{{ js_manifest.features.moderation | relative_url }}" crossorigin="anonymous" />
128
+ {% endif %}
80
129
  {% if js_manifest.features and js_manifest.features.notebook and page.notebook_post %}
81
130
  <link rel="modulepreload" href="{{ js_manifest.features.notebook | relative_url }}" crossorigin="anonymous" />
82
131
  {% endif %}
@@ -106,11 +155,11 @@
106
155
  <meta name="msapplication-TileColor" content="#0b1d3d" />
107
156
 
108
157
  {% if page.seo_title %}
109
- <title>{{ page.seo_title | strip_html | normalize_whitespace }} | {{ site.title }}</title>
158
+ <title>{{ page.seo_title | normalize_whitespace | escape }} | {{ site.title | escape }}</title>
110
159
  {% elsif page.title and page.title != site.title %}
111
- <title>{{ page.title | strip_html | normalize_whitespace }} | {{ site.title }}</title>
160
+ <title>{{ page.title | normalize_whitespace | escape }} | {{ site.title | escape }}</title>
112
161
  {% else %}
113
- <title>{{ site.title }}</title>
162
+ <title>{{ site.title | escape }}</title>
114
163
  {% endif %}
115
164
 
116
165
  {%- comment -%}
@@ -304,47 +353,10 @@
304
353
  {% endif %}
305
354
  {% endif %}
306
355
 
307
- {% assign citation_source = page.citation_authors | default: page.authors %}
308
- {% if page.title or page.citation_title %}
309
- <meta name="citation_title" content="{{ page.citation_title | default: page_title | strip_html | normalize_whitespace }}" />
310
- {% endif %}
311
- {% if citation_source %}
312
- {% capture citation_source_json %}{{ citation_source | jsonify }}{% endcapture %}
313
- {% assign citation_source_first = citation_source_json | slice: 0, 1 %}
314
- {% if citation_source_first == '[' %}
315
- {% assign citation_author_list = citation_source %}
316
- {% else %}
317
- {% assign citation_author_list = citation_source | replace: ';', ',' | split: ',' %}
318
- {% endif %}
319
- {% for author in citation_author_list %}
320
- <meta name="citation_author" content="{{ author | strip | escape }}" />
321
- {% endfor %}
322
- {% elsif page.author or site.author.name %}
323
- <meta name="citation_author" content="{{ page.author | default: site.author.name | strip | escape }}" />
324
- {% endif %}
325
- {% if page.date %}
326
- <meta name="citation_publication_date" content="{{ page.date | date: '%Y/%m/%d' }}" />
327
- {% endif %}
328
- {% if page.doi %}
329
- <meta name="citation_doi" content="{{ page.doi }}" />
330
- {% endif %}
331
- {% if page.journal %}
332
- <meta name="citation_journal_title" content="{{ page.journal }}" />
333
- {% endif %}
334
- {% if page.conference %}
335
- <meta name="citation_conference_title" content="{{ page.conference }}" />
336
- {% endif %}
337
- {% if page.orcid or site.author.orcid %}
338
- <meta name="citation_orcid" content="{{ page.orcid | default: site.author.orcid }}" />
339
- {% endif %}
340
- {% if page.dataset_doi %}
341
- <meta name="citation_dataset_doi" content="{{ page.dataset_doi }}" />
342
- {% endif %}
343
- {% if page.citation_pdf %}
344
- <link rel="citation_pdf_url" href="{{ page.citation_pdf | absolute_url }}" />
345
- {% endif %}
356
+ {%- comment -%} Highwire and Dublin Core tags, for a page read as a research article; the include decides which. {%- endcomment -%}
357
+ {% include meta/scholarly.html %}
346
358
 
347
- <link rel="alternate" type="application/atom+xml" title="{{ site.title }}" href="{{ '/feed.xml' | absolute_url }}" />
359
+ <link rel="alternate" type="application/atom+xml" title="{{ site.title | escape }}" href="{{ '/feed.xml' | absolute_url }}" />
348
360
  <meta http-equiv="Permissions-Policy" content="interest-cohort=()" />
349
361
  {%- comment -%} A verification tag renders only when the site sets its code: an empty one verifies nothing. {%- endcomment %}
350
362
  {% if site.google_site_verification and site.google_site_verification != "" %}
@@ -35,9 +35,9 @@ page language gets its own copy of any translated descriptions.
35
35
  {%- assign nav_description = nav_section | default: nav_title -%}
36
36
  {%- assign nav_description = nav_description | append: ' resources' -%}
37
37
  {%- endif -%}
38
- <li class="nav-item" data-section="{{ item.section | default: nav_title | downcase }}">
38
+ <li class="nav-item" data-section="{{ item.section | default: nav_title | downcase | escape }}">
39
39
  <a class="nav-link{% if is_active %} is-active{% endif %}" href="{{ item_url }}"{% if is_active %} aria-current="page"{% endif %}>
40
- <span class="nav-title">{{ nav_title }}</span>
40
+ <span class="nav-title">{{ nav_title | escape }}</span>
41
41
  <span class="nav-description">{{ nav_description | strip }}</span>
42
42
  </a>
43
43
  </li>
@@ -39,7 +39,7 @@ and is cached for each distinct set.
39
39
  </svg>
40
40
  </span>
41
41
  <span class="site-title-group">
42
- <span class="site-title">{{ site.name | default: site.title }}</span>
42
+ <span class="site-title">{{ site.name | default: site.title | escape }}</span>
43
43
  <span class="site-tagline">{{ brand_symbol }}</span>
44
44
  </span>
45
45
  </a>
@@ -24,10 +24,10 @@ Renders the standard article scaffold used by the default layout.
24
24
  page or layout that renders its own heading, so the page has one.
25
25
  {%- endcomment -%}
26
26
  {%- if article_page.title and article_page.show_title != false and layout.show_title != false -%}
27
- <h1 class="article-title" itemprop="headline">{{ article_page.title }}</h1>
27
+ <h1 class="article-title" itemprop="headline">{{ article_page.title | escape }}</h1>
28
28
  {%- endif -%}
29
29
  {%- if article_page.subtitle -%}
30
- <p class="article-subtitle">{{ article_page.subtitle }}</p>
30
+ <p class="article-subtitle">{{ article_page.subtitle | escape }}</p>
31
31
  {%- endif -%}
32
32
  {%- endunless -%}
33
33
  {%- if article_page.excerpt -%}
@@ -52,14 +52,12 @@ Renders the standard article scaffold used by the default layout.
52
52
  </dd>
53
53
  </div>
54
54
  {%- endif -%}
55
- {%- assign author_name = article_page.author | default: site.author.name -%}
55
+ {%- assign article_authors = article_page | page_authors -%}
56
56
  {%- comment -%} `show_author: false` hides the byline, for pages that are site chrome rather than articles. {%- endcomment -%}
57
- {%- if author_name and article_page.show_author != false -%}
57
+ {%- if article_authors.size > 0 and article_page.show_author != false -%}
58
58
  <div>
59
- <dt class="visually-hidden">{% t 'article.meta.author' %}</dt>
60
- <dd itemprop="author" itemscope itemtype="https://schema.org/Person">
61
- <span itemprop="name">{{ author_name }}</span>
62
- </dd>
59
+ <dt class="visually-hidden">{% if article_authors.size > 1 %}{% t 'article.meta.authors' %}{% else %}{% t 'article.meta.author' %}{% endif %}</dt>
60
+ <dd>{% include components/author-list.html people=article_authors show_affiliation=false %}</dd>
63
61
  </div>
64
62
  {%- endif -%}
65
63
  </dl>