datalog-theme 0.7.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +188 -0
- data/CITATION.cff +2 -2
- data/README.md +25 -17
- data/_data/cdn-integrity.yml +0 -30
- data/_data/i18n/en.yml +297 -0
- data/_data/i18n/es.yml +297 -0
- data/_data/i18n/pt.yml +297 -0
- data/_data/js_manifest.json +16 -0
- data/_includes/analytics/dashboard.html +3 -1
- data/_includes/components/api-function.html +20 -1
- data/_includes/components/author-bio.html +21 -11
- data/_includes/components/author-list.html +32 -0
- data/_includes/components/citation-tools.html +33 -25
- data/_includes/components/comments-thread.html +90 -0
- data/_includes/components/contact-form.html +120 -0
- data/_includes/components/correction-report.html +82 -0
- data/_includes/components/enhanced-code-block.html +1 -1
- data/_includes/components/enhanced-toc.html +6 -7
- data/_includes/components/license-link.html +13 -0
- data/_includes/components/license-notice.html +28 -0
- data/_includes/components/moderation-inbox.html +139 -0
- data/_includes/components/reactions.html +49 -0
- data/_includes/components/reading-list.html +31 -0
- data/_includes/components/reading-mode-toggle.html +65 -0
- data/_includes/components/reading-state-bookmark.html +27 -0
- data/_includes/components/reading-state-panel.html +71 -0
- data/_includes/components/reproducibility.html +61 -0
- data/_includes/components/responsive-image.html +3 -3
- data/_includes/components/revision-history.html +39 -0
- data/_includes/components/revision-notice.html +35 -0
- data/_includes/components/series-nav.html +64 -0
- data/_includes/components/subscribe-form.html +88 -0
- data/_includes/components/subscription-manage.html +68 -0
- data/_includes/components/webmentions.html +48 -0
- data/_includes/csp-meta.html +135 -11
- data/_includes/footer.html +22 -20
- data/_includes/head.html +111 -63
- data/_includes/header/navigation.html +12 -15
- data/_includes/header.html +28 -19
- data/_includes/layouts/default/article.html +9 -7
- data/_includes/meta/dynamic-services-config.html +14 -0
- data/_includes/meta/math-config.html +18 -10
- data/_includes/meta/person-json.html +27 -0
- data/_includes/meta/publisher.html +45 -0
- data/_includes/meta/schema.html +67 -30
- data/_includes/meta/scholarly.html +121 -0
- data/_includes/meta/scripts-loader.html +18 -32
- data/_includes/meta/webmention-discovery.html +14 -0
- data/_includes/post/related-posts.html +4 -7
- data/_includes/scripts.html +57 -0
- data/_includes/search/index-data.json +9 -34
- data/_layouts/dataset.html +5 -3
- data/_layouts/default.html +16 -8
- data/_layouts/notebook.html +1 -0
- data/_layouts/package.html +4 -2
- data/_layouts/page.html +15 -0
- data/_layouts/portfolio.html +1 -0
- data/_layouts/post.html +102 -23
- data/_layouts/project.html +4 -3
- data/_layouts/research.html +20 -8
- data/_plugins/analytics_dashboard.rb +9 -3
- data/_plugins/authors.rb +133 -0
- data/_plugins/config_validator.rb +231 -23
- data/_plugins/critical_css_check.rb +42 -0
- data/_plugins/csp_generator.rb +18 -28
- data/_plugins/datalog_bibliography.rb +9 -7
- data/_plugins/datalog_comments.rb +8 -5
- data/_plugins/datalog_slides.rb +9 -8
- data/_plugins/i18n.rb +13 -12
- data/_plugins/image_optimizer.rb +241 -178
- data/_plugins/licenses.rb +135 -0
- data/_plugins/math_preprocessor.rb +59 -7
- data/_plugins/notebook_converter.rb +23 -4
- data/_plugins/plugin_loader.rb +3 -1
- data/_plugins/publications_generator.rb +8 -2
- data/_plugins/references.rb +238 -0
- data/_plugins/reproducibility.rb +150 -0
- data/_plugins/revisions.rb +101 -0
- data/_plugins/rouge_highlight_filter.rb +42 -0
- data/_plugins/scholarly.rb +50 -0
- data/_plugins/search_code_blocks.rb +30 -0
- data/_plugins/search_normalizer.rb +15 -53
- data/_plugins/search_pages.rb +3 -4
- data/_plugins/series.rb +104 -0
- data/_plugins/statements.rb +87 -0
- data/_sass/_academic-dashboard.scss +262 -0
- data/_sass/_base.scss +20 -1
- data/_sass/_comments-thread.scss +159 -0
- data/_sass/_components.scss +64 -1153
- data/_sass/_features.scss +17 -0
- data/_sass/_layout.scss +385 -1
- data/_sass/_mathematical.scss +27 -0
- data/_sass/_moderation.scss +222 -0
- data/_sass/_notebooks.scss +322 -0
- data/_sass/_open-science-badges.scss +56 -0
- data/_sass/{_phase1-enhancements.scss → _post-components.scss} +5 -3
- data/_sass/_print.scss +291 -0
- data/_sass/_reactions.scss +89 -0
- data/_sass/_reading-state.scss +290 -0
- data/_sass/_search-page.scss +530 -0
- data/_sass/_search.scss +46 -0
- data/_sass/_service-forms.scss +204 -0
- data/_sass/_subscriptions.scss +140 -0
- data/_sass/_syntax-highlighting.scss +212 -97
- data/_sass/_theme.scss +40 -19
- data/_sass/_typography.scss +130 -0
- data/_sass/_utilities.scss +5 -0
- data/_sass/_variables.scss +6 -0
- data/_sass/_webmentions.scss +125 -0
- data/assets/css/main.scss +14 -0
- data/assets/js/dist/academic.js +1 -1
- data/assets/js/dist/analytics-dashboard.js +1 -1
- data/assets/js/dist/chunks/chunk-2DYDWUFX.js +1 -0
- data/assets/js/dist/chunks/chunk-PATLC23F.js +1 -0
- data/assets/js/dist/chunks/chunk-V7734B2G.js +1 -0
- data/assets/js/dist/comments.js +2 -0
- data/assets/js/dist/contact.js +1 -0
- data/assets/js/dist/core.js +1 -1
- data/assets/js/dist/corrections.js +1 -0
- data/assets/js/dist/loader.js +1 -1
- data/assets/js/dist/math.js +1 -1
- data/assets/js/dist/moderation.js +1 -0
- data/assets/js/dist/notebook.js +1 -1
- data/assets/js/dist/reactions.js +1 -0
- data/assets/js/dist/reading-state.js +1 -0
- data/assets/js/dist/search.js +1 -1
- data/assets/js/dist/sources.json +52 -0
- data/assets/js/dist/subscriptions.js +1 -0
- data/assets/js/dist/visualizations.js +11 -2
- data/assets/js/dist/webmentions.js +1 -0
- data/assets/js/loader.js +37 -1
- data/datalog-theme.gemspec +35 -23
- data/lib/datalog/cli.rb +43 -15
- data/lib/datalog/critical_css.rb +168 -0
- data/lib/datalog/plugin_system/dependency_resolver.rb +0 -2
- data/lib/datalog/plugins/comments.rb +33 -3
- data/lib/datalog/theme/installed_files.rb +113 -0
- data/lib/datalog/theme/package.rb +57 -0
- data/lib/datalog/theme/repository_checkout.rb +94 -0
- data/lib/datalog/theme/version.rb +5 -1
- data/lib/datalog/warning_filter.rb +5 -11
- data/lib/datalog-theme.rb +6 -0
- metadata +96 -147
- data/_data/academic.yml +0 -217
- data/_data/config/author.yml +0 -121
- data/_data/datasets.yml +0 -28
- data/_data/js_meta.json +0 -371
- data/_data/navigation.yml +0 -145
- data/_data/projects.yml +0 -41
- data/_data/publications.yml +0 -28
- data/_data/social.yml +0 -73
- data/_data/visualizations.yml +0 -51
- data/_includes/components/advanced-search.html +0 -682
- data/_includes/components/bookmark-system.html +0 -96
- data/_includes/components/comments.html +0 -244
- data/_includes/components/content-recommendations.html +0 -228
- data/_includes/components/email-preferences.html +0 -200
- data/_includes/components/enhanced-metadata.html +0 -228
- data/_includes/components/language-switcher.html +0 -396
- data/_includes/components/navigation-enhancements.html +0 -454
- data/_includes/components/newsletter-signup.html +0 -178
- data/_includes/components/popular-posts.html +0 -233
- data/_includes/components/reading-progress.html +0 -133
- data/_includes/components/reading-time.html +0 -121
- data/_includes/components/series-navigation.html +0 -124
- data/_includes/components/social-proof.html +0 -34
- data/_includes/components/user-preferences.html +0 -566
- data/_includes/meta/syntax-config.html +0 -19
- data/_layouts/archive.html +0 -282
- data/_layouts/post-sidebar.html +0 -183
- data/_sass/_phase3-enhancements.scss +0 -874
- data/_sass/_phase4-enhancements.scss +0 -1214
- data/_sass/_phase5-enhancements.scss +0 -414
- data/assets/js/academic.js +0 -262
- data/assets/js/analytics-dashboard.js +0 -382
- data/assets/js/core/dark-mode.js +0 -79
- data/assets/js/core/github-cards.js +0 -123
- data/assets/js/core/language-filter.js +0 -69
- data/assets/js/core/navigation.js +0 -184
- data/assets/js/core/scroll-progress.js +0 -45
- data/assets/js/core/search-hotkeys.js +0 -62
- data/assets/js/core/skip-links.js +0 -62
- data/assets/js/dist/manifest.json +0 -22
- data/assets/js/dist/meta.json +0 -371
- data/assets/js/main.js +0 -23
- data/assets/js/math.js +0 -818
- data/assets/js/notebook.js +0 -158
- data/assets/js/search/analytics.js +0 -91
- data/assets/js/search/app.js +0 -271
- data/assets/js/search/autocomplete.js +0 -120
- data/assets/js/search/engine.js +0 -260
- data/assets/js/search/filters.js +0 -45
- data/assets/js/search/render.js +0 -217
- data/assets/js/search/utils.js +0 -99
- data/assets/js/search.js +0 -354
- data/assets/js/visualizations.js +0 -816
- data/assets/publications/datalog-publications.bib +0 -8
- data/assets/publications/datalog-publications.ris +0 -9
- data/assets/publications/publications.bib +0 -30
- data/assets/templates/diogo-ribeiro-cv.md +0 -31
- data/assets/templates/diogo-ribeiro-cv.tex +0 -32
- data/lib/datalog/theme/theme.rb +0 -18
data/_plugins/authors.rb
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Datalog
|
|
4
|
+
# One list of a page's authors, and one of its contributors, which the
|
|
5
|
+
# byline, the author cards, the JSON-LD, the citation meta tags and the
|
|
6
|
+
# citation exports all read. Each used to work the author out on its own: a
|
|
7
|
+
# post named one author, `author: john_doe` put "john_doe" in the byline while
|
|
8
|
+
# the card looked the key up in _data/authors.yml, the research layout gave
|
|
9
|
+
# every author the site author's ORCID and affiliation, and the JSON-LD listed
|
|
10
|
+
# the site author's profiles only when an ORCID was set.
|
|
11
|
+
#
|
|
12
|
+
# An entry is a name, a key of _data/authors.yml, or a map with `name` (or
|
|
13
|
+
# `id`, a key of _data/authors.yml). The data record and, for the site's own
|
|
14
|
+
# author, `author` in _config.yml fill in what the entry leaves out.
|
|
15
|
+
module Authors
|
|
16
|
+
module_function
|
|
17
|
+
|
|
18
|
+
PROFILE_URLS = {
|
|
19
|
+
"github" => "https://github.com/%s",
|
|
20
|
+
"twitter" => "https://twitter.com/%s",
|
|
21
|
+
"linkedin" => "https://linkedin.com/in/%s"
|
|
22
|
+
}.freeze
|
|
23
|
+
PROFILE_LINKS = %w[researchgate google_scholar].freeze
|
|
24
|
+
ORCID_ID = /\A\d{4}-\d{4}-\d{4}-\d{3}[\dX]\z/
|
|
25
|
+
|
|
26
|
+
# `authors:`, else `author:`, else the site's author.
|
|
27
|
+
def authors(page, site)
|
|
28
|
+
entries = entries(value(page, "authors"))
|
|
29
|
+
entries = entries(value(page, "author")) if entries.empty?
|
|
30
|
+
records = entries.filter_map { |entry| resolve(entry, site) }
|
|
31
|
+
records = [site_author(site)].compact if records.empty?
|
|
32
|
+
apply_page_affiliation(records, page)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def contributors(page, site)
|
|
36
|
+
entries(value(page, "contributors")).filter_map { |entry| resolve(entry, site) }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# A list, one entry, or names in a string separated by semicolons.
|
|
40
|
+
def entries(value)
|
|
41
|
+
list = if value.is_a?(String) then value.split(";")
|
|
42
|
+
elsif value.is_a?(Hash) then [value]
|
|
43
|
+
else Array(value)
|
|
44
|
+
end
|
|
45
|
+
list.reject { |entry| entry.nil? || (entry.is_a?(String) && entry.strip.empty?) }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# A plain entry is a key or a name; a map's own fields win over the record's.
|
|
49
|
+
def resolve(entry, site)
|
|
50
|
+
own = entry.is_a?(Hash) ? present(entry) : {}
|
|
51
|
+
key = own["id"] || own["name"] || entry.to_s.strip
|
|
52
|
+
record = known(key, site).merge(own)
|
|
53
|
+
record["name"] ||= key
|
|
54
|
+
return if record["name"].to_s.strip.empty?
|
|
55
|
+
|
|
56
|
+
finish(record, site)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# The _data/authors.yml record for a key, or the site author's profile
|
|
60
|
+
# when the key is the site author's name.
|
|
61
|
+
def known(key, site)
|
|
62
|
+
data = value(value(site, "data"), "authors")
|
|
63
|
+
record = value(data, key.to_s)
|
|
64
|
+
return present(record) if record.is_a?(Hash)
|
|
65
|
+
|
|
66
|
+
author = site_author_profile(site)
|
|
67
|
+
author && author["name"] == key ? author : {}
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def site_author(site)
|
|
71
|
+
profile = site_author_profile(site)
|
|
72
|
+
profile && finish(profile, site)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def site_author_profile(site)
|
|
76
|
+
author = value(site, "author")
|
|
77
|
+
return { "name" => author.strip } if author.is_a?(String) && !author.strip.empty?
|
|
78
|
+
|
|
79
|
+
present(author) if author.is_a?(Hash) && author["name"]
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def finish(record, site)
|
|
83
|
+
record["affiliation"] ||= record["institution"]
|
|
84
|
+
record["bio"] ||= record["biography"]
|
|
85
|
+
record["image"] ||= record["avatar"] || record["photo"]
|
|
86
|
+
record["orcid"] = "https://orcid.org/#{record['orcid']}" if record["orcid"].to_s.match?(ORCID_ID)
|
|
87
|
+
record["site_author"] = record["name"] == site_author_profile(site)&.fetch("name")
|
|
88
|
+
record["same_as"] = profiles(record)
|
|
89
|
+
record
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def profiles(record)
|
|
93
|
+
urls = [record["orcid"]]
|
|
94
|
+
urls += PROFILE_URLS.map { |key, template| format(template, record[key]) if record[key] }
|
|
95
|
+
urls += PROFILE_LINKS.map { |key| record[key] }
|
|
96
|
+
urls.compact.map(&:to_s).reject(&:empty?).uniq
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# `author_affiliation` in front matter names the affiliation of a page's
|
|
100
|
+
# only author; an empty one removes it.
|
|
101
|
+
def apply_page_affiliation(records, page)
|
|
102
|
+
affiliation = value(page, "author_affiliation")
|
|
103
|
+
return records if affiliation.nil? || records.size != 1
|
|
104
|
+
|
|
105
|
+
affiliation.to_s.strip.empty? ? records.first.delete("affiliation") : records.first["affiliation"] = affiliation
|
|
106
|
+
records
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# String keys, without the blank values an entry leaves for its record to fill.
|
|
110
|
+
def present(hash)
|
|
111
|
+
hash.to_h.each_with_object({}) do |(key, entry), result|
|
|
112
|
+
result[key.to_s] = entry unless entry.nil? || (entry.is_a?(String) && entry.strip.empty?)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Pages are hashes in Liquid, documents and the site are drops; both answer [].
|
|
117
|
+
def value(object, key)
|
|
118
|
+
object[key] if object.respond_to?(:[]) && !object.is_a?(String) && !object.is_a?(Array)
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
module AuthorFilters
|
|
123
|
+
def page_authors(page)
|
|
124
|
+
Authors.authors(page, @context["site"])
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def page_contributors(page)
|
|
128
|
+
Authors.contributors(page, @context["site"])
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
Liquid::Template.register_filter(Datalog::AuthorFilters)
|
|
@@ -3,23 +3,141 @@
|
|
|
3
3
|
require "uri"
|
|
4
4
|
|
|
5
5
|
module Datalog
|
|
6
|
+
# The dynamic-services settings are inlined into every page for the
|
|
7
|
+
# browser (docs/dynamic-services.md), so a key that names a secret is a
|
|
8
|
+
# leak whatever its value; the validator reports each one.
|
|
9
|
+
module PublicConfiguration
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
# `credentials` is fetch's cookie mode, not a secret, so "credential" is not in the list.
|
|
13
|
+
SECRET_KEY = /secret|token|password|passwd|api_key|apikey|private_key/i
|
|
14
|
+
|
|
15
|
+
# The blocks of _config.yml that reach the browser as they are. `moderation`
|
|
16
|
+
# is where an admin password is most tempting and least use: a static page
|
|
17
|
+
# cannot keep one (docs/moderation.md).
|
|
18
|
+
PUBLIC_BLOCKS = %w[dynamic_services moderation].freeze
|
|
19
|
+
|
|
20
|
+
def errors(config)
|
|
21
|
+
PUBLIC_BLOCKS.flat_map do |block|
|
|
22
|
+
settings = config[block]
|
|
23
|
+
next [] unless settings.is_a?(Hash)
|
|
24
|
+
|
|
25
|
+
secret_keys(settings, [block]).map { |path| secret_error(block, path) }
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def secret_error(block, path)
|
|
30
|
+
{
|
|
31
|
+
headline: "Secret in public configuration '#{path.join('.')}'",
|
|
32
|
+
path: path,
|
|
33
|
+
expected: "no credential: #{block} is sent to every reader's browser, so a key, token or " \
|
|
34
|
+
"password belongs on the server, never in _config.yml",
|
|
35
|
+
actual: "a key named '#{path.last}'"
|
|
36
|
+
}
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def secret_keys(hash, path)
|
|
40
|
+
hash.flat_map do |key, value|
|
|
41
|
+
here = path + [key.to_s]
|
|
42
|
+
found = key.to_s.match?(SECRET_KEY) ? [here] : []
|
|
43
|
+
value.is_a?(Hash) ? found + secret_keys(value, here) : found
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
6
48
|
class ConfigValidator < Jekyll::Generator
|
|
7
49
|
safe true
|
|
8
50
|
priority :highest
|
|
9
51
|
|
|
10
|
-
|
|
52
|
+
# docs/ is not part of the built site, so errors link to the file on GitHub.
|
|
53
|
+
DOCUMENTATION_URL = "https://github.com/DiogoRibeiro7/analytics-blog-jekyll/blob/main/docs/configuration-reference.md"
|
|
54
|
+
|
|
55
|
+
# A licence: an identifier such as CC-BY-4.0 or MIT, or a map naming one.
|
|
56
|
+
LICENSE = {
|
|
57
|
+
type: %i[string hash],
|
|
58
|
+
schema: {
|
|
59
|
+
id: { type: :string },
|
|
60
|
+
name: { type: :string },
|
|
61
|
+
url: { type: :string },
|
|
62
|
+
holder: { type: %i[string array] },
|
|
63
|
+
year: { type: %i[integer string] }
|
|
64
|
+
}
|
|
65
|
+
}.freeze
|
|
11
66
|
|
|
12
67
|
SCHEMA = {
|
|
13
68
|
title: { type: :string, required: true },
|
|
14
69
|
url: { type: :string, required: true, format: :url },
|
|
70
|
+
# `author: Jane Doe` is a common Jekyll setting; the hash adds an email and profile links.
|
|
15
71
|
author: {
|
|
16
|
-
type:
|
|
72
|
+
type: %i[string hash],
|
|
17
73
|
required: true,
|
|
18
74
|
schema: {
|
|
19
75
|
name: { type: :string, required: true },
|
|
20
76
|
email: { type: :string, format: :email }
|
|
21
77
|
}
|
|
22
78
|
},
|
|
79
|
+
# Who publishes the site in structured data and citations. Without it, the author does.
|
|
80
|
+
publisher: {
|
|
81
|
+
type: :hash,
|
|
82
|
+
schema: {
|
|
83
|
+
type: { type: :string, enum: %w[Person Organization] },
|
|
84
|
+
name: { type: :string },
|
|
85
|
+
url: { type: :string },
|
|
86
|
+
logo: { type: :string }
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
# The licence of the site's articles (text and figures) and of their code samples.
|
|
90
|
+
content_license: LICENSE,
|
|
91
|
+
code_license: LICENSE,
|
|
92
|
+
# Which pages get scholarly discovery metadata: true for every post, or a list of collections and layouts.
|
|
93
|
+
scholarly: { type: %i[boolean array string] },
|
|
94
|
+
dynamic_services: {
|
|
95
|
+
type: :hash,
|
|
96
|
+
schema: {
|
|
97
|
+
base_url: { type: :string, format: :url }, api_version: { type: %i[string integer] },
|
|
98
|
+
timeout_ms: { type: :integer }, credentials: { type: :string, enum: %w[omit same-origin include] },
|
|
99
|
+
features: { type: :hash }, paths: { type: :hash }, csrf_header: { type: :string }, csrf_cookie: { type: :string }
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
# The correction-report form on posts (components/correction-report.html).
|
|
103
|
+
corrections: {
|
|
104
|
+
type: :hash,
|
|
105
|
+
schema: { enabled: { type: :boolean }, categories: { type: :array } }
|
|
106
|
+
},
|
|
107
|
+
# The contact and collaboration form (components/contact-form.html).
|
|
108
|
+
contact: {
|
|
109
|
+
type: :hash,
|
|
110
|
+
schema: {
|
|
111
|
+
enabled: { type: :boolean }, categories: { type: :array }, prompts: { type: :hash },
|
|
112
|
+
privacy_notice: { type: :string }, retention: { type: :string }
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
# "Was this useful?" after a post (components/reactions.html).
|
|
116
|
+
reactions: {
|
|
117
|
+
type: :hash,
|
|
118
|
+
schema: { enabled: { type: :boolean }, counts: { type: :boolean }, types: { type: :array } }
|
|
119
|
+
},
|
|
120
|
+
# Webmentions: the receiver advertised in the head, the mentions shown under a post.
|
|
121
|
+
webmentions: {
|
|
122
|
+
type: :hash,
|
|
123
|
+
schema: { enabled: { type: :boolean }, endpoint: { type: :string }, types: { type: :array } }
|
|
124
|
+
},
|
|
125
|
+
# The moderation inbox (docs/moderation.md): public settings only, never a credential.
|
|
126
|
+
moderation: {
|
|
127
|
+
type: :hash,
|
|
128
|
+
schema: {
|
|
129
|
+
enabled: { type: :boolean }, endpoint: { type: :string }, sign_in_url: { type: :string },
|
|
130
|
+
credentials: { type: :string, enum: %w[omit same-origin include] }
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
# The newsletter form and the page its emails link to (components/subscribe-form.html).
|
|
134
|
+
subscriptions: {
|
|
135
|
+
type: :hash,
|
|
136
|
+
schema: {
|
|
137
|
+
enabled: { type: :boolean }, double_opt_in: { type: :boolean }, placement: { type: :array },
|
|
138
|
+
topics: { type: :array }, privacy_url: { type: :string }
|
|
139
|
+
}
|
|
140
|
+
},
|
|
23
141
|
markdown: { type: :string, enum: %w[kramdown commonmark] },
|
|
24
142
|
highlighter: { type: :string, enum: %w[rouge pygments] },
|
|
25
143
|
permalink: { type: :string },
|
|
@@ -69,10 +187,23 @@ module Datalog
|
|
|
69
187
|
enabled: { type: :boolean }
|
|
70
188
|
}
|
|
71
189
|
},
|
|
72
|
-
|
|
190
|
+
# The "Reading mode" control on posts, and whether a reader's choice is kept.
|
|
191
|
+
reading_mode: {
|
|
73
192
|
type: :hash,
|
|
74
193
|
schema: {
|
|
75
|
-
|
|
194
|
+
enabled: { type: :boolean },
|
|
195
|
+
remember: { type: :boolean }
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
# Bookmarks, reading progress and private highlights, kept in the reader's browser.
|
|
199
|
+
reading_state: {
|
|
200
|
+
type: :hash,
|
|
201
|
+
schema: {
|
|
202
|
+
enabled: { type: :boolean },
|
|
203
|
+
bookmarks: { type: :boolean },
|
|
204
|
+
progress: { type: :boolean },
|
|
205
|
+
highlights: { type: :boolean },
|
|
206
|
+
list_url: { type: :string }
|
|
76
207
|
}
|
|
77
208
|
}
|
|
78
209
|
}
|
|
@@ -84,6 +215,15 @@ module Datalog
|
|
|
84
215
|
replacement: "theme_options.math.engine",
|
|
85
216
|
message: "'math_engine' has moved under theme_options.math.engine.",
|
|
86
217
|
auto_migrate: true
|
|
218
|
+
},
|
|
219
|
+
"theme_options.syntax_highlighting" => {
|
|
220
|
+
message: "Code is highlighted by Rouge when the site builds and the theme no longer loads Prism, " \
|
|
221
|
+
"so these settings have no effect. Remove them."
|
|
222
|
+
},
|
|
223
|
+
# The user guide's troubleshooting table sent readers here.
|
|
224
|
+
"theme_options.math.enabled" => {
|
|
225
|
+
message: "Nothing reads it: theme_options.math.render_on_load decides which pages load the math engine, " \
|
|
226
|
+
"and a page's `math` front matter overrides that. Remove it."
|
|
87
227
|
}
|
|
88
228
|
}.freeze
|
|
89
229
|
|
|
@@ -94,6 +234,32 @@ module Datalog
|
|
|
94
234
|
}
|
|
95
235
|
}.freeze
|
|
96
236
|
|
|
237
|
+
# Pointing layouts_dir and the rest into a copy of the theme is how a site
|
|
238
|
+
# used a Git submodule before the theme loaded its own plugins. Jekyll
|
|
239
|
+
# cannot read the theme's assets or _data that way, so those were copied
|
|
240
|
+
# into the site, and the copies fall behind the theme on every update.
|
|
241
|
+
module ThemeDirectories
|
|
242
|
+
module_function
|
|
243
|
+
|
|
244
|
+
URL = "https://github.com/DiogoRibeiro7/analytics-blog-jekyll/blob/main/docs/install.md#keep-the-theme-in-a-git-submodule"
|
|
245
|
+
KEYS = [%w[layouts_dir], %w[includes_dir], %w[plugins_dir], %w[sass sass_dir]].freeze
|
|
246
|
+
|
|
247
|
+
def warnings(config)
|
|
248
|
+
source = File.expand_path(config["source"] || Dir.pwd)
|
|
249
|
+
keys = KEYS.select { |path| Array(config.dig(*path)).any? { |dir| theme_checkout?(dir, source) } }
|
|
250
|
+
return [] if keys.empty?
|
|
251
|
+
|
|
252
|
+
["#{keys.map { |path| path.join('.') }.join(', ')} point into a copy of datalog-theme. Jekyll cannot " \
|
|
253
|
+
"read the theme's assets and _data that way, so a site copies them in, and the copies fall behind " \
|
|
254
|
+
"the theme. Use the copy as the theme instead: #{URL}"]
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def theme_checkout?(dir, source)
|
|
258
|
+
checkout = File.dirname(File.expand_path(dir.to_s, source))
|
|
259
|
+
checkout != source && File.file?(File.join(checkout, "datalog-theme.gemspec"))
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
|
|
97
263
|
class Validator
|
|
98
264
|
attr_reader :errors, :warnings
|
|
99
265
|
|
|
@@ -109,6 +275,7 @@ module Datalog
|
|
|
109
275
|
|
|
110
276
|
apply_deprecated_migrations
|
|
111
277
|
validate_schema(@config, SCHEMA)
|
|
278
|
+
check_math_defaults
|
|
112
279
|
|
|
113
280
|
self
|
|
114
281
|
end
|
|
@@ -144,6 +311,36 @@ module Datalog
|
|
|
144
311
|
end
|
|
145
312
|
end
|
|
146
313
|
|
|
314
|
+
# With render_on_load: auto, only the pages with math load the math
|
|
315
|
+
# engine. `math: true` or `mathjax: true` in front matter defaults loads it
|
|
316
|
+
# on every page in their scope instead, and nothing in the build said so.
|
|
317
|
+
def check_math_defaults
|
|
318
|
+
return unless dig_value(@config, %w[theme_options math render_on_load]) == "auto"
|
|
319
|
+
|
|
320
|
+
Array(@config["defaults"]).each do |entry|
|
|
321
|
+
values = entry["values"] if entry.is_a?(Hash)
|
|
322
|
+
next unless values.is_a?(Hash)
|
|
323
|
+
|
|
324
|
+
%w[math mathjax].each do |key|
|
|
325
|
+
next unless values[key] == true
|
|
326
|
+
|
|
327
|
+
@warnings << "Front matter defaults set '#{key}: true' for #{describe_scope(entry['scope'])}, so they all " \
|
|
328
|
+
"load the math engine, whatever theme_options.math.render_on_load: auto finds. " \
|
|
329
|
+
"Remove it, and set 'math: true' on the pages that need the engine."
|
|
330
|
+
end
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def describe_scope(scope)
|
|
335
|
+
scope = {} unless scope.is_a?(Hash)
|
|
336
|
+
type = scope["type"].to_s
|
|
337
|
+
path = scope["path"].to_s
|
|
338
|
+
return "every page" if type.empty? && path.empty?
|
|
339
|
+
|
|
340
|
+
pages = type.empty? ? "the pages" : "the pages of type '#{type}'"
|
|
341
|
+
path.empty? ? pages : "#{pages} under '#{path}'"
|
|
342
|
+
end
|
|
343
|
+
|
|
147
344
|
def validate_schema(data, schema, path = [])
|
|
148
345
|
schema.each do |key, rules|
|
|
149
346
|
key_path = path + [key.to_s]
|
|
@@ -168,6 +365,10 @@ module Datalog
|
|
|
168
365
|
end
|
|
169
366
|
|
|
170
367
|
def validate_type(path, value, rules)
|
|
368
|
+
# YAML never yields a Symbol, but converters rewrite the site's own
|
|
369
|
+
# configuration: jekyll-sass-converter turns sass.style into one.
|
|
370
|
+
value = value.to_s if value.is_a?(Symbol)
|
|
371
|
+
|
|
171
372
|
expected_type = rules[:type]
|
|
172
373
|
if expected_type && !type_valid?(value, expected_type)
|
|
173
374
|
errors << build_type_error(path, expected_type, value)
|
|
@@ -204,6 +405,8 @@ module Datalog
|
|
|
204
405
|
end
|
|
205
406
|
|
|
206
407
|
def type_valid?(value, expected_type)
|
|
408
|
+
return expected_type.any? { |type| type_valid?(value, type) } if expected_type.is_a?(Array)
|
|
409
|
+
|
|
207
410
|
case expected_type
|
|
208
411
|
when :string
|
|
209
412
|
value.is_a?(String)
|
|
@@ -222,6 +425,8 @@ module Datalog
|
|
|
222
425
|
|
|
223
426
|
def url_valid?(value)
|
|
224
427
|
return false unless value.is_a?(String)
|
|
428
|
+
# `url: ""` is what `jekyll new` writes, and what a site built locally keeps.
|
|
429
|
+
return true if value.strip.empty?
|
|
225
430
|
|
|
226
431
|
uri = URI.parse(value)
|
|
227
432
|
uri.is_a?(URI::HTTP) && !uri.host.nil?
|
|
@@ -241,8 +446,7 @@ module Datalog
|
|
|
241
446
|
headline: "Missing required configuration '#{identifier}'",
|
|
242
447
|
path: path,
|
|
243
448
|
expected: "Value required",
|
|
244
|
-
actual: "nil"
|
|
245
|
-
doc_url: documentation_url(path)
|
|
449
|
+
actual: "nil"
|
|
246
450
|
}
|
|
247
451
|
end
|
|
248
452
|
|
|
@@ -252,8 +456,7 @@ module Datalog
|
|
|
252
456
|
headline: "Invalid type for '#{identifier}'",
|
|
253
457
|
path: path,
|
|
254
458
|
expected: human_type(expected_type),
|
|
255
|
-
actual: value.inspect
|
|
256
|
-
doc_url: documentation_url(path)
|
|
459
|
+
actual: value.inspect
|
|
257
460
|
}
|
|
258
461
|
end
|
|
259
462
|
|
|
@@ -265,8 +468,7 @@ module Datalog
|
|
|
265
468
|
path: path,
|
|
266
469
|
expected: enum_values.map { |v| "\"#{v}\"" }.join(" or "),
|
|
267
470
|
actual: value.inspect,
|
|
268
|
-
suggestion: suggestion
|
|
269
|
-
doc_url: documentation_url(path)
|
|
471
|
+
suggestion: suggestion
|
|
270
472
|
}
|
|
271
473
|
end
|
|
272
474
|
|
|
@@ -276,8 +478,7 @@ module Datalog
|
|
|
276
478
|
headline: "Invalid value for '#{identifier}'",
|
|
277
479
|
path: path,
|
|
278
480
|
expected: expectation,
|
|
279
|
-
actual: value.inspect
|
|
280
|
-
doc_url: documentation_url(path)
|
|
481
|
+
actual: value.inspect
|
|
281
482
|
}
|
|
282
483
|
end
|
|
283
484
|
|
|
@@ -300,17 +501,14 @@ module Datalog
|
|
|
300
501
|
lines << " Received: #{error[:actual]}" if error[:actual]
|
|
301
502
|
lines << ""
|
|
302
503
|
lines << " #{error[:suggestion]}" if error[:suggestion]
|
|
303
|
-
lines << " Documentation: #{
|
|
504
|
+
lines << " Documentation: #{DOCUMENTATION_URL}"
|
|
304
505
|
lines.compact!
|
|
305
506
|
lines.join("\n")
|
|
306
507
|
end
|
|
307
508
|
|
|
308
|
-
def documentation_url(path)
|
|
309
|
-
anchor = path.map { |segment| segment.gsub(/[^a-z0-9]+/i, "-") }.join("-").downcase
|
|
310
|
-
"#{DOCUMENTATION_BASE_URL}##{anchor}"
|
|
311
|
-
end
|
|
312
|
-
|
|
313
509
|
def human_type(type)
|
|
510
|
+
return type.map { |entry| human_type(entry) }.join(" or ") if type.is_a?(Array)
|
|
511
|
+
|
|
314
512
|
case type
|
|
315
513
|
when :string then "a String"
|
|
316
514
|
when :integer then "an Integer"
|
|
@@ -339,7 +537,8 @@ module Datalog
|
|
|
339
537
|
head, *tail = path
|
|
340
538
|
return nil if head.nil?
|
|
341
539
|
|
|
342
|
-
|
|
540
|
+
# `data[head] || data[head.to_sym]` turned a `false` into nil.
|
|
541
|
+
value = data.key?(head) ? data[head] : data[head.to_sym]
|
|
343
542
|
return value if tail.empty?
|
|
344
543
|
|
|
345
544
|
dig_value(value, tail)
|
|
@@ -375,17 +574,26 @@ module Datalog
|
|
|
375
574
|
end
|
|
376
575
|
end
|
|
377
576
|
|
|
577
|
+
# `jekyll serve` processes the same site again on every change, without
|
|
578
|
+
# reading _config.yml again, and by then converters have rewritten parts of
|
|
579
|
+
# the configuration: jekyll-sass-converter turns sass.style into a Symbol.
|
|
580
|
+
# Checking on every build stopped each rebuild, so a site is checked on its
|
|
581
|
+
# first build. A `:site, :after_init` hook would also run once, but Jekyll's
|
|
582
|
+
# command line reports an error raised there as a backtrace, not a message.
|
|
378
583
|
def generate(site)
|
|
584
|
+
return if @validated
|
|
585
|
+
|
|
379
586
|
validator = Validator.new(site.config)
|
|
380
587
|
validator.run
|
|
588
|
+
# The Validator class is at its length limit; the public-configuration check lives beside it.
|
|
589
|
+
validator.errors.concat(PublicConfiguration.errors(site.config))
|
|
381
590
|
|
|
382
|
-
validator.warnings.each do |warning|
|
|
591
|
+
(validator.warnings + ThemeDirectories.warnings(site.config)).each do |warning|
|
|
383
592
|
logger.warn("config", warning)
|
|
384
593
|
end
|
|
385
594
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
raise Jekyll::Errors::FatalException, validator.formatted_errors
|
|
595
|
+
@validated = validator.errors.empty?
|
|
596
|
+
raise Jekyll::Errors::FatalException, validator.formatted_errors unless @validated
|
|
389
597
|
end
|
|
390
598
|
|
|
391
599
|
private
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Datalog
|
|
4
|
+
# The theme ships _includes/critical-css/*.html empty, since critical CSS
|
|
5
|
+
# depends on a site's own pages. A production build with critical_css.enabled
|
|
6
|
+
# and no critical CSS written inlined nothing and kept main.css blocking
|
|
7
|
+
# rendering, and nothing said so.
|
|
8
|
+
module CriticalCssCheck
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
TARGETS = %w[home post default].freeze
|
|
12
|
+
|
|
13
|
+
def warning(site)
|
|
14
|
+
return unless Jekyll.env == "production"
|
|
15
|
+
|
|
16
|
+
settings = site.config["critical_css"]
|
|
17
|
+
return unless settings.is_a?(Hash) && settings["enabled"] == true
|
|
18
|
+
|
|
19
|
+
empty = TARGETS.select { |target| fragment(site, target).strip.empty? }
|
|
20
|
+
return if empty.empty?
|
|
21
|
+
|
|
22
|
+
files = empty.map { |target| "_includes/critical-css/#{target}.html" }.join(", ")
|
|
23
|
+
"critical_css.enabled is true, but #{files} #{empty.size == 1 ? 'is' : 'are'} empty, so pages with " \
|
|
24
|
+
"#{empty.size == 1 ? 'that layout' : 'those layouts'} inline no critical CSS and main.css blocks rendering. " \
|
|
25
|
+
"Run `bundle exec datalog critical-css` before building."
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# The site's file, or the theme's when the site has none.
|
|
29
|
+
def fragment(site, target)
|
|
30
|
+
name = File.join("critical-css", "#{target}.html")
|
|
31
|
+
directories = [site.in_source_dir(site.config["includes_dir"].to_s)]
|
|
32
|
+
directories << site.theme.includes_path if site.theme&.includes_path
|
|
33
|
+
path = directories.map { |directory| File.join(directory, name) }.find { |candidate| File.file?(candidate) }
|
|
34
|
+
path ? File.read(path) : ""
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
Jekyll::Hooks.register :site, :post_read do |site|
|
|
40
|
+
warning = Datalog::CriticalCssCheck.warning(site)
|
|
41
|
+
Jekyll.logger.warn("Critical CSS:", warning) if warning
|
|
42
|
+
end
|
data/_plugins/csp_generator.rb
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "securerandom"
|
|
4
|
-
require "digest"
|
|
5
4
|
|
|
6
5
|
module Datalog
|
|
7
6
|
module Security
|
|
@@ -18,7 +17,6 @@ module Datalog
|
|
|
18
17
|
def generate(site)
|
|
19
18
|
site.data["csp"] ||= {}
|
|
20
19
|
site.data["csp"]["nonces"] ||= {}
|
|
21
|
-
site.data["csp"]["hashes"] ||= {}
|
|
22
20
|
|
|
23
21
|
assign_nonces(site, site.pages)
|
|
24
22
|
site.collections.each_value do |collection|
|
|
@@ -40,7 +38,6 @@ module Datalog
|
|
|
40
38
|
|
|
41
39
|
nonce = SecureRandom.base64(nonce_bytes)
|
|
42
40
|
document.data["csp_nonce"] = nonce
|
|
43
|
-
document.data["csp_hashes"] ||= []
|
|
44
41
|
|
|
45
42
|
registry_site = site || (document.respond_to?(:site) ? document.site : nil)
|
|
46
43
|
if registry_site.respond_to?(:data)
|
|
@@ -61,7 +58,12 @@ module Datalog
|
|
|
61
58
|
end
|
|
62
59
|
end
|
|
63
60
|
|
|
64
|
-
|
|
61
|
+
# Gives every inline script in the rendered page the page's nonce. The
|
|
62
|
+
# generator also took a SHA-256 of each inline script, but the policy is
|
|
63
|
+
# written into the head while the page renders, so the hashes of the
|
|
64
|
+
# finished page never reached it; with every script nonced they are not
|
|
65
|
+
# needed.
|
|
66
|
+
def self.add_nonces(document)
|
|
65
67
|
return unless document.respond_to?(:output)
|
|
66
68
|
|
|
67
69
|
output = document.output
|
|
@@ -77,39 +79,27 @@ module Datalog
|
|
|
77
79
|
attributes = Regexp.last_match(1)
|
|
78
80
|
"<script nonce=\"#{nonce}\"#{attributes}>"
|
|
79
81
|
end
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
output.scan(%r{<script(?![^>]*\bsrc=)[^>]*>(.*?)</script>}mi) do |match|
|
|
85
|
-
content = match.first
|
|
86
|
-
next if content.nil? || content.empty?
|
|
87
|
-
|
|
88
|
-
hashes << Digest::SHA256.base64digest(content)
|
|
82
|
+
# A template that printed page.csp_nonce before the page had one left
|
|
83
|
+
# nonce="", which authorises nothing.
|
|
84
|
+
document.output = output.gsub(/(<(?:script|style)\b[^>]*\bnonce=)""/i) do
|
|
85
|
+
"#{Regexp.last_match(1)}\"#{nonce}\""
|
|
89
86
|
end
|
|
90
|
-
|
|
91
|
-
hashes.uniq!
|
|
92
|
-
document.data["csp_hashes"] = hashes
|
|
93
|
-
|
|
94
|
-
return unless site.respond_to?(:data)
|
|
95
|
-
|
|
96
|
-
site.data["csp"] ||= {}
|
|
97
|
-
site.data["csp"]["hashes"] ||= {}
|
|
98
|
-
|
|
99
|
-
key = document_key(document)
|
|
100
|
-
|
|
101
|
-
site.data["csp"]["hashes"][key] = hashes if key
|
|
102
87
|
end
|
|
103
88
|
end
|
|
104
89
|
end
|
|
105
90
|
end
|
|
106
91
|
|
|
107
92
|
%i[pages documents].each do |target|
|
|
108
|
-
Jekyll::Hooks.register target, :pre_render do |document|
|
|
109
|
-
Datalog::Security::CspGenerator.assign_nonce(document.respond_to?(:site) ? document.site : nil, document)
|
|
93
|
+
Jekyll::Hooks.register target, :pre_render do |document, payload|
|
|
94
|
+
nonce = Datalog::Security::CspGenerator.assign_nonce(document.respond_to?(:site) ? document.site : nil, document)
|
|
95
|
+
# A page another generator creates after this one runs, such as a notebook
|
|
96
|
+
# page, only gets its nonce here. Jekyll has already copied a page's data
|
|
97
|
+
# into the hash its templates read, so page.csp_nonce rendered empty there.
|
|
98
|
+
page_data = payload && payload["page"]
|
|
99
|
+
page_data["csp_nonce"] ||= nonce if nonce && page_data.is_a?(Hash)
|
|
110
100
|
end
|
|
111
101
|
|
|
112
102
|
Jekyll::Hooks.register target, :post_render do |document|
|
|
113
|
-
Datalog::Security::CspGenerator.
|
|
103
|
+
Datalog::Security::CspGenerator.add_nonces(document)
|
|
114
104
|
end
|
|
115
105
|
end
|
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Jekyll
|
|
4
|
+
# Stands in for the `datalog_bibliography` tag when the datalog-citations
|
|
5
|
+
# plugin is not enabled, so a layout that uses the tag still builds. The plugin
|
|
6
|
+
# registers the real tag and sets `datalog_bibliography` on the pages it
|
|
7
|
+
# handles; a page that sets the key by hand without the plugin gets nothing and
|
|
8
|
+
# a build warning, where it used to get a "coming soon" notice.
|
|
4
9
|
class DatalogBibliographyTag < Liquid::Tag
|
|
5
10
|
def render(context)
|
|
6
11
|
page = context.registers[:page]
|
|
7
|
-
|
|
8
|
-
return "" unless bib_data
|
|
12
|
+
return "" unless page["datalog_bibliography"]
|
|
9
13
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
</div>
|
|
14
|
-
HTML
|
|
14
|
+
Jekyll.logger.warn("datalog-citations", "#{page['path']} sets datalog_bibliography, " \
|
|
15
|
+
"but datalog_plugins.enabled does not list datalog-citations")
|
|
16
|
+
""
|
|
15
17
|
end
|
|
16
18
|
end
|
|
17
19
|
end
|