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
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uri"
|
|
4
|
+
|
|
5
|
+
module Datalog
|
|
6
|
+
# The computational artifacts behind an article, for the "Reproduce this
|
|
7
|
+
# analysis" panel (_includes/components/reproducibility.html):
|
|
8
|
+
#
|
|
9
|
+
# reproducibility:
|
|
10
|
+
# code:
|
|
11
|
+
# url: https://github.com/example/project
|
|
12
|
+
# ref: 4f2c1ab # the commit, tag or branch the article used
|
|
13
|
+
# data:
|
|
14
|
+
# doi: 10.5281/zenodo.1234567 # or url:
|
|
15
|
+
# version: v2
|
|
16
|
+
# environment:
|
|
17
|
+
# file: requirements.txt # in the code repository at the ref, a site path or a URL
|
|
18
|
+
# container: ghcr.io/example/project:1.4.0
|
|
19
|
+
# archive: https://doi.org/10.5281/zenodo.7654321
|
|
20
|
+
# notebook:
|
|
21
|
+
# url: /notebooks/example/
|
|
22
|
+
# results:
|
|
23
|
+
# url: https://github.com/example/project/releases/tag/results-v1
|
|
24
|
+
# version: results-v1
|
|
25
|
+
#
|
|
26
|
+
# Every artifact is optional, and each may be a bare URL. The panel shows
|
|
27
|
+
# what is given and claims nothing more: a link is a link, a ref is a ref.
|
|
28
|
+
# A URL that is not http(s), a site path or a DOI stops the build.
|
|
29
|
+
module Reproducibility
|
|
30
|
+
module_function
|
|
31
|
+
|
|
32
|
+
KINDS = %w[code data notebook environment results].freeze
|
|
33
|
+
# Where a ref and a file in the repository can be linked.
|
|
34
|
+
HOSTS = {
|
|
35
|
+
"github.com" => { tree: "/tree/%s", blob: "/blob/%s/%s" },
|
|
36
|
+
"gitlab.com" => { tree: "/-/tree/%s", blob: "/-/blob/%s/%s" }
|
|
37
|
+
}.freeze
|
|
38
|
+
URL = %r{\Ahttps?://[^\s"'<>]+\z}i
|
|
39
|
+
PATH = %r{\A/[^\s"'<>]*\z}
|
|
40
|
+
|
|
41
|
+
# The artifacts, in KINDS order, or nil when the page gives none.
|
|
42
|
+
def resolve(page, site)
|
|
43
|
+
value = Authors.value(page, "reproducibility")
|
|
44
|
+
return unless value.is_a?(Hash)
|
|
45
|
+
|
|
46
|
+
given = value.transform_keys(&:to_s)
|
|
47
|
+
baseurl = Authors.value(site, "baseurl").to_s
|
|
48
|
+
artifacts = KINDS.filter_map { |kind| artifact(kind, given[kind], given, page, baseurl) }
|
|
49
|
+
{ "artifacts" => artifacts } unless artifacts.empty?
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def artifact(kind, value, all, page, baseurl)
|
|
53
|
+
return if value.nil? || value == false || (value.is_a?(String) && value.strip.empty?)
|
|
54
|
+
|
|
55
|
+
given = value.is_a?(Hash) ? Authors.present(value) : { "url" => value.to_s }
|
|
56
|
+
url = link(given["url"] || doi_url(given["doi"]), page, "#{kind}.url", baseurl)
|
|
57
|
+
artifact = { "kind" => kind, "url" => url, "external" => external?(url), "version" => given["version"]&.to_s,
|
|
58
|
+
"label" => given["label"] || display(url), "doi" => bare_doi(given["doi"]) }
|
|
59
|
+
artifact.merge!(code(given, url)) if kind == "code"
|
|
60
|
+
artifact.merge!(environment(given, all, page, baseurl)) if kind == "environment"
|
|
61
|
+
artifact = artifact.compact
|
|
62
|
+
artifact if artifact.values_at("url", "file", "container", "archive").any?
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def code(given, url)
|
|
66
|
+
ref = given["ref"].to_s.strip
|
|
67
|
+
return {} if ref.empty?
|
|
68
|
+
|
|
69
|
+
{ "ref" => ref, "ref_url" => host_url(url, :tree, ref) }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# The environment file lives in the code repository at the article's ref
|
|
73
|
+
# unless it is a site path or a URL of its own.
|
|
74
|
+
def environment(given, all, page, baseurl)
|
|
75
|
+
file = given["file"].to_s.strip
|
|
76
|
+
archive = link(given["archive"], page, "environment.archive", baseurl)
|
|
77
|
+
{
|
|
78
|
+
"file" => (file unless file.empty?),
|
|
79
|
+
"file_url" => (file_url(file, all["code"], page, baseurl) unless file.empty?),
|
|
80
|
+
"container" => given["container"]&.to_s,
|
|
81
|
+
"archive" => archive, "archive_label" => display(archive)
|
|
82
|
+
}
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def file_url(file, code, page, baseurl)
|
|
86
|
+
own = file.match?(URL) || file.match?(PATH) || file.match?(/\Adoi:/i)
|
|
87
|
+
return link(file, page, "environment.file", baseurl) if own
|
|
88
|
+
|
|
89
|
+
code = code.is_a?(Hash) ? Authors.present(code) : { "url" => code.to_s }
|
|
90
|
+
code_url = link(code["url"], page, "code.url", baseurl)
|
|
91
|
+
ref = code["ref"].to_s.strip
|
|
92
|
+
host_url(code_url, :blob, ref.empty? ? "HEAD" : ref, file)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# A page of a known host under the repository URL, such as a tree or a blob.
|
|
96
|
+
def host_url(url, kind, *parts)
|
|
97
|
+
return unless url
|
|
98
|
+
|
|
99
|
+
host = URI.parse(url).host.to_s.sub(/\Awww\./, "")
|
|
100
|
+
pattern = HOSTS.dig(host, kind)
|
|
101
|
+
"#{url.chomp('/')}#{format(pattern, *parts)}" if pattern
|
|
102
|
+
rescue URI::InvalidURIError
|
|
103
|
+
nil
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# An http(s) URL as given, a site path with the baseurl, or a DOI as its
|
|
107
|
+
# URL; anything else, such as a javascript: URL or an address without its
|
|
108
|
+
# scheme, stops the build.
|
|
109
|
+
def link(value, page, field, baseurl)
|
|
110
|
+
text = value.to_s.strip
|
|
111
|
+
return if text.empty?
|
|
112
|
+
return doi_url(text) if text.match?(/\Adoi:/i)
|
|
113
|
+
return text if text.match?(URL)
|
|
114
|
+
return "#{baseurl}#{text}" if text.match?(PATH)
|
|
115
|
+
|
|
116
|
+
raise Jekyll::Errors::FatalException,
|
|
117
|
+
"#{Authors.value(page, 'path')} reproducibility.#{field} is #{text.inspect}, which is not an http(s) " \
|
|
118
|
+
"URL, a site path starting with / or a doi:; give the whole address, such as https://github.com/example/project"
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def bare_doi(doi)
|
|
122
|
+
text = doi.to_s.strip.sub(%r{\Ahttps?://(dx\.)?doi\.org/}i, "").sub(/\Adoi:\s*/i, "")
|
|
123
|
+
text unless text.empty?
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def doi_url(doi)
|
|
127
|
+
bare = bare_doi(doi)
|
|
128
|
+
"https://doi.org/#{bare}" if bare
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# What a link reads: the address without its scheme, or the site path.
|
|
132
|
+
def display(url)
|
|
133
|
+
return url unless external?(url)
|
|
134
|
+
|
|
135
|
+
url.sub(%r{\Ahttps?://(www\.)?}i, "").chomp("/")
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def external?(url)
|
|
139
|
+
url.to_s.match?(URL)
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
module ReproducibilityFilters
|
|
144
|
+
def page_reproducibility(page)
|
|
145
|
+
Reproducibility.resolve(page, @context["site"])
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
Liquid::Template.register_filter(Datalog::ReproducibilityFilters)
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "date"
|
|
4
|
+
require "time"
|
|
5
|
+
|
|
6
|
+
module Datalog
|
|
7
|
+
# The revision history of an article, for a post that is corrected or
|
|
8
|
+
# rewritten while keeping its URL:
|
|
9
|
+
#
|
|
10
|
+
# revisions:
|
|
11
|
+
# - date: 2026-09-16
|
|
12
|
+
# type: correction
|
|
13
|
+
# summary: Replaced the sample-size rules with model diagnostics.
|
|
14
|
+
# details_url: https://github.com/example/repo/pull/425
|
|
15
|
+
# - date: 2024-03-12
|
|
16
|
+
# type: update
|
|
17
|
+
# summary: Updated the code examples for the current SciPy.
|
|
18
|
+
#
|
|
19
|
+
# Before the site renders, each page's list is checked, its dates are
|
|
20
|
+
# parsed and its entries sorted newest first, so the includes read one
|
|
21
|
+
# shape. A correction or an update is substantive: the newest one becomes
|
|
22
|
+
# `revision_notice`, which the post layout announces under the metadata.
|
|
23
|
+
# A review or an editorial change appears in the history only.
|
|
24
|
+
#
|
|
25
|
+
# Every revision but a review changed the article, so the newest one sets
|
|
26
|
+
# `last_modified_at` when it is later than the date the page gives, and
|
|
27
|
+
# the JSON-LD, the microdata, the feed and the sitemap all carry it.
|
|
28
|
+
module Revisions
|
|
29
|
+
module_function
|
|
30
|
+
|
|
31
|
+
TYPES = %w[correction update review editorial].freeze
|
|
32
|
+
SUBSTANTIVE = %w[correction update].freeze
|
|
33
|
+
DEFAULT_TYPE = "update"
|
|
34
|
+
|
|
35
|
+
def normalize!(document)
|
|
36
|
+
data = document.data
|
|
37
|
+
return unless data.key?("revisions")
|
|
38
|
+
|
|
39
|
+
revisions = entries(data["revisions"], document)
|
|
40
|
+
data["revisions"] = revisions
|
|
41
|
+
notice = revisions.find { |revision| revision["substantive"] }
|
|
42
|
+
data["revision_notice"] = notice unless data["revision_notice"] == false
|
|
43
|
+
|
|
44
|
+
changed = revisions.find { |revision| revision["type"] != "review" }
|
|
45
|
+
modified = to_time(data["last_modified_at"] || data["updated"])
|
|
46
|
+
data["last_modified_at"] = changed["date"] if changed && (modified.nil? || changed["date"] > modified)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Newest first; revisions on one day keep their order.
|
|
50
|
+
def entries(list, document)
|
|
51
|
+
unless list.is_a?(Array)
|
|
52
|
+
stop(document, "has revisions that is not a list; each revision is a map with a date, a summary and, " \
|
|
53
|
+
"if wanted, a type and a details_url")
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
revisions = list.each_with_index.map { |entry, index| revision(entry, index + 1, document) }
|
|
57
|
+
revisions.each_with_index.sort_by { |revision, index| [-revision["date"].to_i, index] }.map(&:first)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def revision(entry, number, document)
|
|
61
|
+
stop(document, "revision #{number} is not a map with a date and a summary") unless entry.is_a?(Hash)
|
|
62
|
+
|
|
63
|
+
entry = entry.transform_keys(&:to_s)
|
|
64
|
+
type = (entry["type"] || DEFAULT_TYPE).to_s.strip.downcase
|
|
65
|
+
unless TYPES.include?(type)
|
|
66
|
+
stop(document, "revision #{number} has the type #{entry['type'].inspect}; it takes #{TYPES.join(', ')}")
|
|
67
|
+
end
|
|
68
|
+
summary = entry["summary"].to_s.strip
|
|
69
|
+
stop(document, "revision #{number} needs a summary saying what changed") if summary.empty?
|
|
70
|
+
date = to_time(entry["date"])
|
|
71
|
+
unless date
|
|
72
|
+
stop(document, "revision #{number} has the date #{entry['date'].inspect}, which is not a date such as " \
|
|
73
|
+
"2026-09-16")
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
details = entry["details_url"].to_s.strip
|
|
77
|
+
{ "date" => date, "type" => type, "summary" => summary, "details_url" => (details unless details.empty?),
|
|
78
|
+
"substantive" => SUBSTANTIVE.include?(type) }.compact
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def stop(document, problem)
|
|
82
|
+
raise Jekyll::Errors::FatalException, "#{document.relative_path} #{problem}"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# A Date, a Time or a string naming a day, such as "2026-09-16".
|
|
86
|
+
def to_time(value)
|
|
87
|
+
case value
|
|
88
|
+
when Time then value
|
|
89
|
+
when Date then value.to_time
|
|
90
|
+
when String
|
|
91
|
+
parts = Date._parse(value)
|
|
92
|
+
Time.parse(value) if parts[:year] && parts[:mon] && parts[:mday]
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
Jekyll::Hooks.register :site, :post_read do |site|
|
|
99
|
+
site.documents.each { |document| Datalog::Revisions.normalize!(document) }
|
|
100
|
+
site.pages.each { |page| Datalog::Revisions.normalize!(page) }
|
|
101
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rouge"
|
|
4
|
+
|
|
5
|
+
# `rouge_highlight` highlights code with Rouge as the site builds, the way
|
|
6
|
+
# kramdown highlights fenced code blocks. Includes that print code passed to
|
|
7
|
+
# them, such as `components/api-function.html`, call it as
|
|
8
|
+
# `code | rouge_highlight: language` inside `<pre class="highlight"><code>`;
|
|
9
|
+
# the notebook converter calls `RougeHighlightFilter.highlight` for code cells.
|
|
10
|
+
# The result is escaped; a language Rouge does not know comes back as plain
|
|
11
|
+
# text.
|
|
12
|
+
module Jekyll
|
|
13
|
+
module RougeHighlightFilter
|
|
14
|
+
# kramdown's opening tag for a block Rouge highlighted.
|
|
15
|
+
KRAMDOWN_CODE_BLOCK = '<pre class="highlight">'
|
|
16
|
+
|
|
17
|
+
def self.highlight(code, language = nil)
|
|
18
|
+
return "" if code.nil?
|
|
19
|
+
|
|
20
|
+
source = code.to_s
|
|
21
|
+
lexer = Rouge::Lexer.find_fancy(language.to_s.strip.downcase, source) || Rouge::Lexers::PlainText
|
|
22
|
+
Rouge::Formatters::HTML.new.format(lexer.lex(source))
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def rouge_highlight(code, language = nil)
|
|
26
|
+
RougeHighlightFilter.highlight(code, language)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
Liquid::Template.register_filter(Jekyll::RougeHighlightFilter)
|
|
32
|
+
|
|
33
|
+
# A code block wider than the page scrolls, and a keyboard user can only scroll
|
|
34
|
+
# it once it takes focus. Prism made every block focusable in the browser; the
|
|
35
|
+
# blocks kramdown highlights get the attribute here, and the includes and the
|
|
36
|
+
# notebook converter write it themselves.
|
|
37
|
+
Jekyll::Hooks.register %i[pages documents], :post_convert do |document|
|
|
38
|
+
block = Jekyll::RougeHighlightFilter::KRAMDOWN_CODE_BLOCK
|
|
39
|
+
next unless document.content&.include?(block)
|
|
40
|
+
|
|
41
|
+
document.content = document.content.gsub(block, '<pre class="highlight" tabindex="0">')
|
|
42
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Datalog
|
|
4
|
+
# Which pages get scholarly discovery metadata (_includes/meta/scholarly.html):
|
|
5
|
+
# the Highwire meta tags Google Scholar and reference managers read, and
|
|
6
|
+
# their Dublin Core equivalents. Not every post is a paper, so the tags are
|
|
7
|
+
# opt-in:
|
|
8
|
+
#
|
|
9
|
+
# scholarly: true # front matter: this page is a research article
|
|
10
|
+
# scholarly: false # front matter: this one is not, whatever the site says
|
|
11
|
+
#
|
|
12
|
+
# scholarly: true # _config.yml: every post and research article
|
|
13
|
+
# scholarly: [notebooks] # _config.yml: these collections or layouts as well
|
|
14
|
+
#
|
|
15
|
+
# A page with the research layout, or in a research collection, is
|
|
16
|
+
# scholarly unless it says otherwise.
|
|
17
|
+
module Scholarly
|
|
18
|
+
module_function
|
|
19
|
+
|
|
20
|
+
ALWAYS = %w[research].freeze
|
|
21
|
+
POSTS = %w[posts post].freeze
|
|
22
|
+
|
|
23
|
+
def scholarly?(page, site)
|
|
24
|
+
own = Authors.value(page, "scholarly")
|
|
25
|
+
return own == true unless own.nil?
|
|
26
|
+
|
|
27
|
+
kinds = kinds(Authors.value(site, "scholarly"))
|
|
28
|
+
[Authors.value(page, "layout"), Authors.value(page, "collection")].any? { |kind| kinds.include?(kind.to_s) }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# The layouts and collections the site's setting covers, besides research.
|
|
32
|
+
def kinds(setting)
|
|
33
|
+
case setting
|
|
34
|
+
when true then ALWAYS + POSTS
|
|
35
|
+
when Array then ALWAYS + setting.map(&:to_s)
|
|
36
|
+
when String then ALWAYS + setting.split(/[\s,]+/)
|
|
37
|
+
else ALWAYS
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
module ScholarlyFilters
|
|
43
|
+
# A Liquid filter's name cannot end with "?".
|
|
44
|
+
def scholarly(page) # rubocop:disable Naming/PredicateMethod
|
|
45
|
+
Scholarly.scholarly?(page, @context["site"])
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
Liquid::Template.register_filter(Datalog::ScholarlyFilters)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Datalog
|
|
4
|
+
# Collects the fenced code blocks of every page and document for the search
|
|
5
|
+
# index. The index template used to split `doc.content` on backticks, but
|
|
6
|
+
# Jekyll renders documents before pages, so by the time search.json rendered
|
|
7
|
+
# that content was HTML and every document's code list came out empty.
|
|
8
|
+
module SearchCodeBlocks
|
|
9
|
+
# An opening fence of three or more backticks or tildes with an optional
|
|
10
|
+
# language, the code, and a closing fence of the same characters.
|
|
11
|
+
FENCE = /^ {0,3}(`{3,}|~{3,})[ \t]*([^\s`~{]*)[^\n]*\n(.*?)^ {0,3}\1[ \t]*$/m
|
|
12
|
+
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
def extract(source)
|
|
16
|
+
text = source.to_s.gsub("\r\n", "\n")
|
|
17
|
+
text.scan(FENCE).map do |_fence, language, code|
|
|
18
|
+
{ "language" => language.empty? ? "text" : language.downcase, "code" => code.chomp }
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Content is still the author's source before rendering starts. Collection
|
|
25
|
+
# docs only: site.documents also lists a collection's static files.
|
|
26
|
+
Jekyll::Hooks.register :site, :pre_render do |site|
|
|
27
|
+
(site.pages + site.collections.values.flat_map(&:docs)).each do |item|
|
|
28
|
+
item.data["search_code"] = Datalog::SearchCodeBlocks.extract(item.content)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -1,66 +1,28 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
begin
|
|
4
|
-
require "unicode_normalize"
|
|
5
|
-
UNICODE_NORMALIZE_SUPPORTED = true
|
|
6
|
-
rescue LoadError
|
|
7
|
-
UNICODE_NORMALIZE_SUPPORTED = false
|
|
8
|
-
warn "[search_normalizer] unicode_normalize gem not available; falling back to basic normalization"
|
|
9
|
-
end
|
|
10
|
-
|
|
11
3
|
module Datalog
|
|
12
4
|
module SearchFilters
|
|
13
5
|
module_function
|
|
14
6
|
|
|
7
|
+
# Lower-cases text and strips combining marks, so "Café" and "cafe" match
|
|
8
|
+
# while letters outside ASCII ("ß", "ł", Cyrillic, CJK) are kept. The
|
|
9
|
+
# browser normalizes queries the same way (assets/js/search/utils.js), so
|
|
10
|
+
# the index and the query agree. String#unicode_normalize is part of Ruby:
|
|
11
|
+
# this file used to require it as if it were a gem, fail, and fall back to
|
|
12
|
+
# dropping every character outside ASCII.
|
|
15
13
|
def normalize_search(input)
|
|
16
|
-
|
|
14
|
+
# Invalid byte sequences are dropped first: unicode_normalize raises on them.
|
|
15
|
+
value = input.to_s.scrub("")
|
|
17
16
|
return "" if value.empty?
|
|
18
17
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
normalized.downcase.strip
|
|
26
|
-
rescue StandardError
|
|
27
|
-
input.to_s.downcase
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
def transliterate(value)
|
|
31
|
-
value.encode("ASCII", fallback: lambda { |char|
|
|
32
|
-
approximate_character(char)
|
|
33
|
-
}, invalid: :replace, undef: :replace, replace: "")
|
|
34
|
-
rescue Encoding::UndefinedConversionError, Encoding::InvalidByteSequenceError
|
|
35
|
-
value
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
def approximate_character(char)
|
|
39
|
-
@transliteration_map ||= build_transliteration_map
|
|
40
|
-
@transliteration_map.fetch(char, "")
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
def build_transliteration_map
|
|
44
|
-
basic_map = {}
|
|
45
|
-
|
|
46
|
-
accents = {
|
|
47
|
-
"ÀÁÂÃÄÅàáâãäå" => "a",
|
|
48
|
-
"ÈÉÊËèéêë" => "e",
|
|
49
|
-
"ÌÍÎÏìíîï" => "i",
|
|
50
|
-
"ÒÓÔÕÖØòóôõöø" => "o",
|
|
51
|
-
"ÙÚÛÜùúûü" => "u",
|
|
52
|
-
"Çç" => "c",
|
|
53
|
-
"Ññ" => "n",
|
|
54
|
-
"Ýýÿ" => "y",
|
|
55
|
-
"Ææ" => "ae",
|
|
56
|
-
"Œœ" => "oe"
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
accents.each do |chars, replacement|
|
|
60
|
-
chars.each_char { |char| basic_map[char] = replacement }
|
|
18
|
+
stripped = begin
|
|
19
|
+
value.unicode_normalize(:nfkd).gsub(/\p{Mn}/, "")
|
|
20
|
+
rescue Encoding::CompatibilityError
|
|
21
|
+
# Text in an encoding other than Unicode cannot be normalized, so it is
|
|
22
|
+
# indexed as it is.
|
|
23
|
+
value
|
|
61
24
|
end
|
|
62
|
-
|
|
63
|
-
basic_map
|
|
25
|
+
stripped.downcase.strip
|
|
64
26
|
end
|
|
65
27
|
|
|
66
28
|
def normalize_search_array(values)
|
data/_plugins/search_pages.rb
CHANGED
|
@@ -61,10 +61,9 @@ module Datalog
|
|
|
61
61
|
"title" => search_config["title"] || "Search",
|
|
62
62
|
"permalink" => PAGE_URL,
|
|
63
63
|
"page_classes" => "search-page",
|
|
64
|
-
# Results can contain LaTeX
|
|
65
|
-
#
|
|
66
|
-
"math" => true
|
|
67
|
-
"syntax_highlighting" => true
|
|
64
|
+
# Results can contain LaTeX, so the math engine is wanted here even
|
|
65
|
+
# when the rest of the site loads it only where math appears.
|
|
66
|
+
"math" => true
|
|
68
67
|
)
|
|
69
68
|
page.data["subtitle"] = search_config["subtitle"] if search_config["subtitle"]
|
|
70
69
|
site.pages << page
|
data/_plugins/series.rb
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Datalog
|
|
4
|
+
# Article series: multi-part writing a reader follows in order, whatever
|
|
5
|
+
# was published in between.
|
|
6
|
+
#
|
|
7
|
+
# series: # front matter, as a map
|
|
8
|
+
# id: missing-data
|
|
9
|
+
# title: Missing Data and Statistical Inference
|
|
10
|
+
# order: 3
|
|
11
|
+
#
|
|
12
|
+
# series: missing-data # or flat
|
|
13
|
+
# series_title: Missing Data and Statistical Inference
|
|
14
|
+
# series_order: 3
|
|
15
|
+
#
|
|
16
|
+
# missing-data: # _data/series.yml, so the title lives once
|
|
17
|
+
# title: Missing Data and Statistical Inference
|
|
18
|
+
# description: Four parts, from the missing-data mechanisms to sensitivity analysis.
|
|
19
|
+
#
|
|
20
|
+
# Before the site renders, every part is checked (an order that is missing,
|
|
21
|
+
# not a whole number or taken by another part stops the build), the parts
|
|
22
|
+
# are put in order, and each page's `series` becomes one shape: id, title,
|
|
23
|
+
# description, order, position, count, parts (title, url, order, position,
|
|
24
|
+
# current), previous and next. _includes/components/series-nav.html reads it.
|
|
25
|
+
module Series
|
|
26
|
+
module_function
|
|
27
|
+
|
|
28
|
+
def normalize!(site)
|
|
29
|
+
documents = site.documents + site.pages
|
|
30
|
+
registry = site.data["series"].is_a?(Hash) ? site.data["series"] : {}
|
|
31
|
+
documents.filter_map { |document| part(document) }.group_by { |part| part[:id] }.each do |id, parts|
|
|
32
|
+
check_orders!(id, parts)
|
|
33
|
+
meta = registry[id].is_a?(Hash) ? registry[id] : {}
|
|
34
|
+
resolve!(id, parts.sort_by { |part| part[:order] }, meta)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# The document's part of a series, as {doc, id, order, title}, or nil.
|
|
39
|
+
def part(document)
|
|
40
|
+
data = document.data
|
|
41
|
+
value = data["series"]
|
|
42
|
+
return if value.nil? || value == false || (value.is_a?(String) && value.strip.empty?)
|
|
43
|
+
|
|
44
|
+
given = case value
|
|
45
|
+
when Hash then value.transform_keys(&:to_s)
|
|
46
|
+
when String, Symbol then { "id" => value.to_s }
|
|
47
|
+
else stop(document, "has a series that is neither a name nor a map with an id and an order")
|
|
48
|
+
end
|
|
49
|
+
id = given["id"].to_s.strip
|
|
50
|
+
stop(document, "has a series without an id, such as series: missing-data") if id.empty?
|
|
51
|
+
|
|
52
|
+
order = given["order"] || data["series_order"]
|
|
53
|
+
unless whole_number?(order)
|
|
54
|
+
stop(document, "is part of the series \"#{id}\" without an order; give the part a whole number from 1, " \
|
|
55
|
+
"such as order: 2")
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
{ doc: document, id: id, order: order.to_i, title: given["title"] || data["series_title"] }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def whole_number?(value)
|
|
62
|
+
(value.is_a?(Integer) && value >= 1) || (value.is_a?(String) && value.match?(/\A[1-9]\d*\z/))
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def check_orders!(id, parts)
|
|
66
|
+
parts.group_by { |part| part[:order] }.each_value do |same|
|
|
67
|
+
next if same.size == 1
|
|
68
|
+
|
|
69
|
+
paths = same.map { |part| part[:doc].relative_path }.sort.join(" and ")
|
|
70
|
+
raise Jekyll::Errors::FatalException,
|
|
71
|
+
"#{paths} are both part #{same.first[:order]} of the series \"#{id}\"; each part needs its own order"
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Writes each part's `series` from the ordered parts and the data file's entry.
|
|
76
|
+
def resolve!(id, ordered, meta)
|
|
77
|
+
title = meta["title"] || ordered.filter_map { |part| part[:title] }.first || titleize(id)
|
|
78
|
+
summaries = ordered.each_with_index.map do |part, index|
|
|
79
|
+
{ "title" => part[:doc].data["title"], "url" => part[:doc].url, "order" => part[:order],
|
|
80
|
+
"position" => index + 1 }
|
|
81
|
+
end
|
|
82
|
+
ordered.each_with_index do |part, index|
|
|
83
|
+
part[:doc].data["series"] = {
|
|
84
|
+
"id" => id, "title" => title, "description" => meta["description"],
|
|
85
|
+
"order" => part[:order], "position" => index + 1, "count" => ordered.size,
|
|
86
|
+
"parts" => summaries.map { |summary| summary.merge("current" => summary["url"] == part[:doc].url) },
|
|
87
|
+
"previous" => (summaries[index - 1] if index.positive?), "next" => summaries[index + 1]
|
|
88
|
+
}.compact
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def titleize(id)
|
|
93
|
+
id.tr("-_", " ").split.map(&:capitalize).join(" ")
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def stop(document, problem)
|
|
97
|
+
raise Jekyll::Errors::FatalException, "#{document.relative_path} #{problem}"
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
Jekyll::Hooks.register :site, :post_read do |site|
|
|
103
|
+
Datalog::Series.normalize!(site)
|
|
104
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "cgi"
|
|
4
|
+
|
|
5
|
+
module Datalog
|
|
6
|
+
# Theorems, lemmas and the other statements of mathematical writing, and
|
|
7
|
+
# proofs. Articles marked them with a blockquote and a bold "Theorem 1.",
|
|
8
|
+
# typed by hand:
|
|
9
|
+
#
|
|
10
|
+
# {% theorem id="thm-consistency" title="Consistency" %}
|
|
11
|
+
# Let $\hat\theta_n$ be ...
|
|
12
|
+
# {% endtheorem %}
|
|
13
|
+
#
|
|
14
|
+
# {% proof for="thm-consistency" %}
|
|
15
|
+
# ...
|
|
16
|
+
# {% endproof %}
|
|
17
|
+
#
|
|
18
|
+
# Statements are numbered with figures and tables (_plugins/references.rb):
|
|
19
|
+
# each kind counts on its own, {% ref thm-consistency %} reads "Theorem 1",
|
|
20
|
+
# and a duplicate id stops the build. `label="A"` names a statement
|
|
21
|
+
# "Theorem A" instead of numbering it. A proof is not numbered; with `for`
|
|
22
|
+
# its heading reads "Proof of Theorem 1".
|
|
23
|
+
class StatementTag < Liquid::Block
|
|
24
|
+
def initialize(tag_name, markup, options)
|
|
25
|
+
super
|
|
26
|
+
@kind = tag_name
|
|
27
|
+
@markup = markup
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def render(context)
|
|
31
|
+
attributes = References.attributes(@markup, context)
|
|
32
|
+
id = References.validate_id(attributes["id"], @kind)
|
|
33
|
+
custom = attributes["label"]
|
|
34
|
+
References.validate_label(custom, @kind)
|
|
35
|
+
body = References.markdown(context, super)
|
|
36
|
+
|
|
37
|
+
number = custom ? %( data-ref-number="#{CGI.escapeHTML(custom)}") : ""
|
|
38
|
+
title = attributes["title"].to_s.strip
|
|
39
|
+
title = title.empty? ? "" : %( <span class="datalog-statement__title">(#{CGI.escapeHTML(title)})</span>)
|
|
40
|
+
%(<div class="datalog-statement datalog-statement--#{@kind}" role="group" aria-labelledby="#{id}-heading" ) +
|
|
41
|
+
%(id="#{id}" data-ref-target="#{id}" data-ref-kind="#{@kind}"#{number}>) +
|
|
42
|
+
%(<p class="datalog-statement__heading" id="#{id}-heading">) +
|
|
43
|
+
%(<span class="datalog-ref-label" data-ref-for="#{id}" data-ref-end=""></span>#{title}.</p>) +
|
|
44
|
+
%(<div class="datalog-statement__body">#{body.strip}</div></div>\n)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
class ProofTag < Liquid::Block
|
|
49
|
+
def initialize(tag_name, markup, options)
|
|
50
|
+
super
|
|
51
|
+
@markup = markup
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def render(context)
|
|
55
|
+
attributes = References.attributes(@markup, context)
|
|
56
|
+
target = attributes["for"]
|
|
57
|
+
body = References.markdown(context, super)
|
|
58
|
+
heading_id = "proof-#{target ? References.validate_id(target, 'proof') : Statements.next_proof(context)}"
|
|
59
|
+
heading = if target
|
|
60
|
+
link = %(<a class="datalog-ref" href="##{target}" data-ref="#{target}">#{target}</a>)
|
|
61
|
+
I18n.translate(context, "references.proof_of", "target" => link)
|
|
62
|
+
else
|
|
63
|
+
CGI.escapeHTML(I18n.translate(context, "references.proof"))
|
|
64
|
+
end
|
|
65
|
+
end_mark = %(<p class="datalog-proof__end"><span aria-hidden="true">∎</span></p>)
|
|
66
|
+
end_mark = "" if attributes["qed"] == "false"
|
|
67
|
+
|
|
68
|
+
%(<div class="datalog-proof" role="group" aria-labelledby="#{heading_id}">) +
|
|
69
|
+
%(<p class="datalog-proof__heading" id="#{heading_id}">#{heading}.</p>) +
|
|
70
|
+
%(<div class="datalog-proof__body">#{body.strip}</div>#{end_mark}</div>\n)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
module Statements
|
|
75
|
+
module_function
|
|
76
|
+
|
|
77
|
+
KINDS = %w[theorem lemma proposition corollary definition assumption example remark].freeze
|
|
78
|
+
|
|
79
|
+
# Proofs without a statement are numbered in the page for their heading ids only.
|
|
80
|
+
def next_proof(context)
|
|
81
|
+
context.registers[:datalog_proofs] = context.registers[:datalog_proofs].to_i + 1
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
Datalog::Statements::KINDS.each { |kind| Liquid::Template.register_tag(kind, Datalog::StatementTag) }
|
|
87
|
+
Liquid::Template.register_tag("proof", Datalog::ProofTag)
|