datalog-theme 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +133 -0
- data/CITATION.cff +2 -2
- data/README.md +4 -2
- data/_data/i18n/en.yml +55 -3
- data/_data/i18n/es.yml +55 -3
- data/_data/i18n/pt.yml +55 -3
- data/_includes/analytics/dashboard.html +24 -9
- data/_includes/components/academic-dashboard.html +168 -133
- data/_includes/components/author-bio.html +7 -5
- data/_includes/components/author-list.html +6 -5
- data/_includes/components/citation-tools.html +2 -1
- data/_includes/components/content-provenance.html +19 -1
- data/_includes/components/correction-report.html +15 -8
- data/_includes/components/hero.html +22 -15
- data/_includes/components/package-index.html +121 -0
- data/_includes/components/package-install.html +27 -50
- data/_includes/components/post-list.html +14 -0
- data/_includes/components/responsive-image.html +15 -21
- data/_includes/footer.html +1 -1
- data/_includes/head.html +70 -11
- data/_includes/layouts/default/article.html +76 -46
- data/_includes/meta/dataset-json.html +171 -0
- data/_includes/meta/math-config.html +15 -1
- data/_includes/meta/package-json.html +69 -0
- data/_includes/meta/person-json.html +36 -13
- data/_includes/meta/publisher.html +15 -0
- data/_includes/meta/schema.html +108 -23
- data/_includes/meta/scholarly.html +10 -5
- data/_includes/post/related-posts.html +17 -43
- data/_includes/scripts.html +2 -2
- data/_includes/search/index-data.json +10 -1
- data/_includes/search/page.html +11 -6
- data/_layouts/archive.html +67 -0
- data/_layouts/dataset.html +2 -1
- data/_layouts/default.html +13 -11
- data/_layouts/docs.html +53 -0
- data/_layouts/home.html +30 -1
- data/_layouts/notebook.html +5 -1
- data/_layouts/package.html +38 -21
- data/_layouts/portfolio.html +1 -0
- data/_layouts/post.html +21 -62
- data/_layouts/project.html +2 -1
- data/_layouts/research.html +14 -2
- data/_plugins/analytics_dashboard.rb +4 -1
- data/_plugins/archive.rb +114 -0
- data/_plugins/authors.rb +50 -1
- data/_plugins/citation_exports.rb +12 -1
- data/_plugins/config_validator.rb +77 -2
- data/_plugins/correction_fallback.rb +110 -0
- data/_plugins/image_optimizer.rb +129 -22
- data/_plugins/math_preprocessor.rb +7 -48
- data/_plugins/notebook_converter.rb +44 -5
- data/_plugins/packages.rb +84 -0
- data/_plugins/page_dates.rb +37 -0
- data/_plugins/references.rb +16 -2
- data/_plugins/related_posts.rb +149 -0
- data/_plugins/search_sections.rb +110 -0
- data/_plugins/site_identity.rb +42 -0
- data/_plugins/social_cards.rb +17 -0
- data/_sass/_academic-dashboard.scss +18 -30
- data/_sass/_base.scss +2 -0
- data/_sass/_components.scss +21 -23
- data/_sass/_docs.scss +143 -0
- data/_sass/_features.scss +6 -0
- data/_sass/_layout.scss +65 -6
- data/_sass/_mathematical.scss +61 -19
- data/_sass/_notebooks.scss +1 -32
- data/_sass/_package-docs.scss +6 -9
- data/_sass/_post-components.scss +23 -23
- data/_sass/_print.scss +1 -1
- data/_sass/_search-page.scss +75 -0
- data/_sass/_search.scss +17 -26
- data/_sass/_syntax-highlighting.scss +23 -1
- data/_sass/_theme.scss +1 -0
- data/_sass/_utilities.scss +87 -40
- data/_sass/_variables.scss +31 -29
- data/assets/css/main.scss +2 -1
- data/assets/js/dist/academic.js +1 -1
- data/assets/js/dist/analytics-dashboard.js +1 -1
- data/assets/js/dist/chunks/chunk-TNVD6UAM.js +1 -0
- data/assets/js/dist/comments.js +1 -1
- data/assets/js/dist/contact.js +1 -1
- data/assets/js/dist/core.js +1 -1
- data/assets/js/dist/corrections.js +5 -1
- data/assets/js/dist/loader.js +1 -1
- data/assets/js/dist/math.js +2 -1
- data/assets/js/dist/moderation.js +1 -1
- data/assets/js/dist/reactions.js +1 -1
- data/assets/js/dist/search.js +1 -1
- data/assets/js/dist/sources.json +20 -16
- data/assets/js/dist/subscriptions.js +1 -1
- data/assets/js/loader.js +6 -2
- data/datalog-theme.gemspec +1 -1
- data/lib/datalog/audit/checks.rb +269 -0
- data/lib/datalog/audit/known_keys.rb +61 -0
- data/lib/datalog/audit/site_reader.rb +118 -0
- data/lib/datalog/audit/source_file.rb +85 -0
- data/lib/datalog/audit.rb +145 -0
- data/lib/datalog/citations/bibtex.rb +200 -0
- data/lib/datalog/citations/entry.rb +333 -0
- data/lib/datalog/citations/markup.rb +79 -0
- data/lib/datalog/cli.rb +164 -89
- data/lib/datalog/critical_css.rb +126 -21
- data/lib/datalog/latex_speech/words.json +88 -0
- data/lib/datalog/latex_speech.rb +355 -0
- data/lib/datalog/packages/command.rb +51 -0
- data/lib/datalog/packages/refresh.rb +187 -0
- data/lib/datalog/packages.rb +177 -0
- data/lib/datalog/plugin_system.rb +5 -0
- data/lib/datalog/plugins/citations.rb +325 -109
- data/lib/datalog/site_config.rb +26 -0
- data/lib/datalog/social_cards/font.rb +286 -0
- data/lib/datalog/social_cards/fonts/IBMPlexSans-Regular.ttf +0 -0
- data/lib/datalog/social_cards/fonts/IBMPlexSerif-SemiBold.ttf +0 -0
- data/lib/datalog/social_cards/fonts/OFL.txt +92 -0
- data/lib/datalog/social_cards/geometry.rb +51 -0
- data/lib/datalog/social_cards/outline.rb +54 -0
- data/lib/datalog/social_cards/plain_text.rb +258 -0
- data/lib/datalog/social_cards/template.rb +322 -0
- data/lib/datalog/social_cards/template.svg +17 -0
- data/lib/datalog/social_cards/typesetter.rb +178 -0
- data/lib/datalog/social_cards.rb +369 -0
- data/lib/datalog/theme/updater.rb +307 -0
- data/lib/datalog/theme/version.rb +1 -1
- metadata +46 -9
- data/_includes/components/enhanced-code-block.html +0 -212
- data/_includes/components/performance-monitor.html +0 -170
- data/_includes/components/viz-table-fallback.html +0 -19
- data/_plugins/datalog_bibliography.rb +0 -21
- data/assets/js/dist/chunks/chunk-WIRUK3OZ.js +0 -1
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
|
|
5
|
+
module Datalog
|
|
6
|
+
# The date a page was published, when it has one. Jekyll gives a collection
|
|
7
|
+
# document without a `date:` the time of the build, so `page.date` cannot
|
|
8
|
+
# tell a dated page from an undated one, and an undated package, dataset or
|
|
9
|
+
# notebook was "published" again by every build (#411). A post's date, from
|
|
10
|
+
# its file name, and any `date:` in front matter are real.
|
|
11
|
+
module PageDates
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
# The page's date, or nil when it has none or its date is the build's.
|
|
15
|
+
def published(page, build_time)
|
|
16
|
+
date = page.respond_to?(:[]) && !page.is_a?(String) ? page["date"] : nil
|
|
17
|
+
return if date.nil? || date.to_s.strip.empty?
|
|
18
|
+
|
|
19
|
+
time = date.respond_to?(:to_time) ? date.to_time : Time.parse(date.to_s)
|
|
20
|
+
return if build_time && time.to_i == build_time.to_i
|
|
21
|
+
|
|
22
|
+
date
|
|
23
|
+
rescue ArgumentError
|
|
24
|
+
nil
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
module PageDateFilters
|
|
29
|
+
# `{{ page | published_date | localize_date }}`, nil for an undated page.
|
|
30
|
+
def published_date(page)
|
|
31
|
+
site = @context.registers[:site]
|
|
32
|
+
PageDates.published(page, site&.time)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
Liquid::Template.register_filter(Datalog::PageDateFilters)
|
data/_plugins/references.rb
CHANGED
|
@@ -196,15 +196,29 @@ module Datalog
|
|
|
196
196
|
raise Liquid::ArgumentError, "{% figure id=\"#{id}\" %} needs alt text for its image"
|
|
197
197
|
end
|
|
198
198
|
|
|
199
|
-
|
|
199
|
+
baseurl = context.registers[:site].config["baseurl"].to_s.chomp("/")
|
|
200
|
+
src = "#{baseurl}#{src}" if src.start_with?("/")
|
|
200
201
|
caption = References.inline(References.markdown(context, super))
|
|
201
202
|
classes = ["datalog-figure", attributes["class"]].compact.join(" ")
|
|
202
203
|
|
|
203
204
|
alt = CGI.escapeHTML(attributes["alt"])
|
|
205
|
+
dark = dark_source(attributes, baseurl)
|
|
204
206
|
%(<figure class="#{CGI.escapeHTML(classes)}" id="#{id}" data-ref-target="#{id}" data-ref-kind="figure">) +
|
|
205
|
-
%(<img src="#{CGI.escapeHTML(src)}" alt="#{alt}" loading="lazy" decoding="async">) +
|
|
207
|
+
%(<img src="#{CGI.escapeHTML(src)}" alt="#{alt}" loading="lazy" decoding="async"#{dark}>) +
|
|
206
208
|
%(<figcaption><span class="datalog-ref-label" data-ref-for="#{id}"></span> #{caption}</figcaption></figure>\n)
|
|
207
209
|
end
|
|
210
|
+
|
|
211
|
+
# The same figure exported for a dark page, as `dark_src`. The image
|
|
212
|
+
# pipeline turns it into a <source media="(prefers-color-scheme: dark)">
|
|
213
|
+
# with the variants it built for it. The alt text stays on the one <img>:
|
|
214
|
+
# it is the same figure, and describing it twice would be wrong.
|
|
215
|
+
def dark_source(attributes, baseurl)
|
|
216
|
+
dark = attributes["dark_src"].to_s.strip
|
|
217
|
+
return "" if dark.empty?
|
|
218
|
+
|
|
219
|
+
dark = "#{baseurl}#{dark}" if dark.start_with?("/")
|
|
220
|
+
%( data-dark-src="#{CGI.escapeHTML(dark)}")
|
|
221
|
+
end
|
|
208
222
|
end
|
|
209
223
|
|
|
210
224
|
class TableTag < Liquid::Block
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Datalog
|
|
4
|
+
module RelatedPosts
|
|
5
|
+
# Select recommendations once after Jekyll has read and normalized series.
|
|
6
|
+
# Each field has an inverted index, so a post visits matching candidates
|
|
7
|
+
# rather than comparing itself with every other post on the site.
|
|
8
|
+
class Index
|
|
9
|
+
FIELDS = %w[tags categories keywords].freeze
|
|
10
|
+
DEFAULT_WEIGHTS = { "tags" => 3.0, "categories" => 0.35, "keywords" => 1.5, "text" => 0.0 }.freeze
|
|
11
|
+
|
|
12
|
+
def initialize(site)
|
|
13
|
+
@posts = site.posts.docs
|
|
14
|
+
settings = site.config["related_posts"].is_a?(Hash) ? site.config["related_posts"] : {}
|
|
15
|
+
@limit = Integer(settings.fetch("limit", 3))
|
|
16
|
+
@min_score = Float(settings.fetch("min_score", 0.1))
|
|
17
|
+
@weights = DEFAULT_WEIGHTS.merge(string_keys(settings["weights"] || {}))
|
|
18
|
+
@weights.transform_values! { |value| Float(value) }
|
|
19
|
+
@terms = {}
|
|
20
|
+
@index = Hash.new { |hash, field| hash[field] = Hash.new { |terms, term| terms[term] = [] } }
|
|
21
|
+
@lookup = Hash.new { |hash, key| hash[key] = [] }
|
|
22
|
+
@excluded = @posts.to_h { |post| [post, post.data["related"] == false] }
|
|
23
|
+
index_posts
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def assign!
|
|
27
|
+
results = @posts.to_h do |post|
|
|
28
|
+
[post, post.data["related_posts"] == false ? [] : recommendations(post)]
|
|
29
|
+
end
|
|
30
|
+
results.each { |post, recommendations| post.data["related"] = recommendations.map { |item| summary(item) } }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def recommendations(post)
|
|
34
|
+
selected = []
|
|
35
|
+
used_series = {}
|
|
36
|
+
manual = Array(post.data["related_posts"]).map { |pick| resolve(post, pick) }
|
|
37
|
+
manual.each do |candidate|
|
|
38
|
+
validate_pick!(post, candidate)
|
|
39
|
+
add(selected, used_series, candidate)
|
|
40
|
+
end
|
|
41
|
+
return selected.first(@limit) if selected.size >= @limit
|
|
42
|
+
|
|
43
|
+
ranked = scores(post).sort_by { |candidate, score| [-score, -candidate.date.to_i, candidate.url] }
|
|
44
|
+
ranked.each do |candidate, score|
|
|
45
|
+
next if score < @min_score || !eligible?(post, candidate)
|
|
46
|
+
|
|
47
|
+
add(selected, used_series, candidate)
|
|
48
|
+
break if selected.size >= @limit
|
|
49
|
+
end
|
|
50
|
+
selected
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def index_posts
|
|
56
|
+
@posts.each do |post|
|
|
57
|
+
@terms[post] = {}
|
|
58
|
+
fields = FIELDS + (@weights["text"].positive? ? ["text"] : [])
|
|
59
|
+
fields.each do |field|
|
|
60
|
+
terms = field == "text" ? text_terms(post) : terms_for(post.data[field])
|
|
61
|
+
@terms[post][field] = terms
|
|
62
|
+
terms.each { |term| @index[field][term] << post }
|
|
63
|
+
end
|
|
64
|
+
[post.url, post.relative_path, post.data["title"]].compact.each do |key|
|
|
65
|
+
@lookup[key.to_s.strip.downcase] << post
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def scores(post)
|
|
71
|
+
scores = Hash.new(0.0)
|
|
72
|
+
@terms.fetch(post).each do |field, terms|
|
|
73
|
+
weight = @weights.fetch(field)
|
|
74
|
+
next if weight.zero?
|
|
75
|
+
|
|
76
|
+
terms.each do |term|
|
|
77
|
+
matches = @index[field][term]
|
|
78
|
+
rarity = field == "tags" ? Math.log((@posts.size + 1.0) / (matches.size + 1)) + 1.0 : 1.0
|
|
79
|
+
matches.each { |candidate| scores[candidate] += weight * rarity unless candidate.equal?(post) }
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
scores
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def resolve(post, pick)
|
|
86
|
+
key = pick.to_s.strip.downcase
|
|
87
|
+
matches = @lookup[key].uniq
|
|
88
|
+
return matches.first if matches.one?
|
|
89
|
+
|
|
90
|
+
problem = matches.empty? ? "matches no post" : "matches several posts; use a URL or path"
|
|
91
|
+
raise Jekyll::Errors::FatalException, "#{post.relative_path} related_posts pick #{pick.inspect} #{problem}"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def validate_pick!(post, candidate)
|
|
95
|
+
return if eligible?(post, candidate)
|
|
96
|
+
|
|
97
|
+
raise Jekyll::Errors::FatalException,
|
|
98
|
+
"#{post.relative_path} related_posts pick #{candidate.url.inspect} is the post itself, " \
|
|
99
|
+
"excluded, or already appears in its series navigation"
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def eligible?(post, candidate)
|
|
103
|
+
own_series = series_id(post)
|
|
104
|
+
!candidate.equal?(post) && !@excluded[candidate] && (own_series.nil? || series_id(candidate) != own_series)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def add(selected, used_series, candidate)
|
|
108
|
+
id = series_id(candidate)
|
|
109
|
+
return if selected.include?(candidate) || (id && used_series[id])
|
|
110
|
+
|
|
111
|
+
selected << candidate
|
|
112
|
+
used_series[id] = true if id
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def series_id(post)
|
|
116
|
+
value = post.data["series"]
|
|
117
|
+
value["id"] if value.is_a?(Hash)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def terms_for(value)
|
|
121
|
+
Array(value).map { |term| term.to_s.strip.downcase }.reject(&:empty?).uniq
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def text_terms(post)
|
|
125
|
+
text = "#{post.data['title']} #{post.data['excerpt']}"
|
|
126
|
+
text.downcase.scan(/[[:alnum:]]{3,}/).uniq
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def string_keys(value)
|
|
130
|
+
value.is_a?(Hash) ? value.transform_keys(&:to_s) : {}
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def summary(post)
|
|
134
|
+
excerpt = post.data["excerpt"] || (post.excerpt if post.respond_to?(:excerpt))
|
|
135
|
+
{ "title" => post.data["title"], "url" => post.url, "teaser" => post.data["teaser"],
|
|
136
|
+
"date" => post.date, "excerpt" => excerpt.to_s, "tags" => post.data["tags"] }
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
class RelatedPostsGenerator < Jekyll::Generator
|
|
142
|
+
safe true
|
|
143
|
+
priority :low
|
|
144
|
+
|
|
145
|
+
def generate(site)
|
|
146
|
+
RelatedPosts::Index.new(site).assign!
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "nokogiri"
|
|
4
|
+
|
|
5
|
+
module Datalog
|
|
6
|
+
# Splits a rendered document into the passages a reader would recognise: the
|
|
7
|
+
# text under each h2 or h3, keyed by the id the heading already carries.
|
|
8
|
+
#
|
|
9
|
+
# A search result used to point at a whole page. For the long methods posts
|
|
10
|
+
# this theme is built for, that hands the reader a second search problem:
|
|
11
|
+
# find the paragraph. The headings and their ids were already in the page —
|
|
12
|
+
# kramdown writes them, the contents list links to them — and the index was
|
|
13
|
+
# the only thing that never recorded which passage sat under which (#336).
|
|
14
|
+
module SearchSections
|
|
15
|
+
HEADINGS = %w[h2 h3].freeze
|
|
16
|
+
# Their text is markup, not prose: a MathJax configuration block or a
|
|
17
|
+
# JSON-LD blob is not something anyone searches for.
|
|
18
|
+
IGNORED = %w[script style template].freeze
|
|
19
|
+
# What cannot sit in a URL fragment. A page whose Liquid has not run when
|
|
20
|
+
# the index renders can carry a heading id of "{{ group_name | slugify }}",
|
|
21
|
+
# and a link to that fragment would go nowhere; the section still indexes,
|
|
22
|
+
# pointing at the page.
|
|
23
|
+
UNUSABLE_ANCHOR = %r{[\s"'<>{}#%\\/]}
|
|
24
|
+
|
|
25
|
+
module_function
|
|
26
|
+
|
|
27
|
+
# Takes the document's own rendered HTML, not the finished page, so the
|
|
28
|
+
# headings a layout puts around the article — "About this post", "Reading
|
|
29
|
+
# notes" — cannot turn up as a section of every result.
|
|
30
|
+
def split(html)
|
|
31
|
+
sections = [section_for(nil)]
|
|
32
|
+
collect(Nokogiri::HTML5.fragment(html.to_s), sections)
|
|
33
|
+
sections.filter_map { |section| finish(section) }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Walks in document order, opening a section at every heading. An element
|
|
37
|
+
# holding a heading somewhere inside it is walked into rather than taken
|
|
38
|
+
# whole, so a wrapper around part of the article does not swallow the
|
|
39
|
+
# boundaries within it.
|
|
40
|
+
def collect(node, sections)
|
|
41
|
+
node.children.each do |child|
|
|
42
|
+
next unless child.element? || child.text?
|
|
43
|
+
next if child.element? && IGNORED.include?(child.name)
|
|
44
|
+
|
|
45
|
+
if child.element? && HEADINGS.include?(child.name)
|
|
46
|
+
sections << section_for(child)
|
|
47
|
+
elsif child.element? && child.css(HEADINGS.join(", ")).any?
|
|
48
|
+
collect(child, sections)
|
|
49
|
+
else
|
|
50
|
+
sections.last[:parts] << prose(child)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# An element taken whole may still hold a script or a style somewhere
|
|
56
|
+
# inside it — a visualization block carries the code that draws it — and
|
|
57
|
+
# that code is not prose. Liquid's `strip_html` drops those blocks with
|
|
58
|
+
# their contents, so the flattened text of the page does not have them
|
|
59
|
+
# either.
|
|
60
|
+
def prose(node)
|
|
61
|
+
return node.text unless node.element? && node.css(IGNORED.join(", ")).any?
|
|
62
|
+
|
|
63
|
+
copy = node.dup
|
|
64
|
+
copy.css(IGNORED.join(", ")).each(&:remove)
|
|
65
|
+
copy.text
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def section_for(heading)
|
|
69
|
+
{
|
|
70
|
+
title: heading ? squeeze(heading.text) : "",
|
|
71
|
+
# A heading with no id — a site that turns kramdown's auto_ids off —
|
|
72
|
+
# still indexes; its section simply links to the page.
|
|
73
|
+
anchor: heading && present(heading["id"]),
|
|
74
|
+
level: heading ? heading.name[1].to_i : 0,
|
|
75
|
+
parts: []
|
|
76
|
+
}
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# The lead paragraphs before the first heading are a section of their own,
|
|
80
|
+
# untitled; so is the whole of a page that has no headings at all.
|
|
81
|
+
def finish(section)
|
|
82
|
+
content = squeeze(section[:parts].join(" "))
|
|
83
|
+
return if section[:title].empty? && content.empty?
|
|
84
|
+
|
|
85
|
+
{ "title" => section[:title], "anchor" => section[:anchor],
|
|
86
|
+
"level" => section[:level], "content" => content }
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def squeeze(text)
|
|
90
|
+
text.to_s.gsub(/\s+/, " ").strip
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def present(value)
|
|
94
|
+
value = value.to_s.strip
|
|
95
|
+
return if value.empty? || value.match?(UNUSABLE_ANCHOR)
|
|
96
|
+
|
|
97
|
+
value
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# `{{ content | markdownify | search_sections }}` in the search index, over
|
|
102
|
+
# the same HTML the flattened text is taken from.
|
|
103
|
+
module SearchSectionsFilter
|
|
104
|
+
def search_sections(html)
|
|
105
|
+
SearchSections.split(html)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
Liquid::Template.register_filter(Datalog::SearchSectionsFilter)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Datalog
|
|
4
|
+
# The site as an entity of its own, for the `WebSite` node that
|
|
5
|
+
# meta/schema.html writes on the homepage. Search engines read that node as
|
|
6
|
+
# the strongest statement of a site's name, and its `alternateName` as the
|
|
7
|
+
# other names people know it by, such as a handle (#322).
|
|
8
|
+
#
|
|
9
|
+
# Off unless `site_identity` in _config.yml asks for it, so a site that wrote
|
|
10
|
+
# its own WebSite JSON-LD does not end up with two that disagree:
|
|
11
|
+
#
|
|
12
|
+
# site_identity: true # the site's title is its name
|
|
13
|
+
# site_identity:
|
|
14
|
+
# name: Jane Doe # default: title
|
|
15
|
+
# alternate_names: [janedoe] # or alternate_name: janedoe
|
|
16
|
+
module SiteIdentity
|
|
17
|
+
module_function
|
|
18
|
+
|
|
19
|
+
# Nil when the site has not asked for it, else its name and other names.
|
|
20
|
+
def resolve(site)
|
|
21
|
+
config = Authors.value(site, "site_identity")
|
|
22
|
+
config = {} if config == true
|
|
23
|
+
return unless config.is_a?(Hash) && config["enabled"] != false
|
|
24
|
+
|
|
25
|
+
name = config["name"].to_s.strip
|
|
26
|
+
name = Authors.value(site, "title").to_s.strip if name.empty?
|
|
27
|
+
return if name.empty?
|
|
28
|
+
|
|
29
|
+
others = Authors.list(config, "alternate_names", "alternate_name")
|
|
30
|
+
{ "name" => name, "alternate_names" => (others - [name]).uniq }
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
module SiteIdentityFilters
|
|
35
|
+
# `{% assign identity = site | site_identity %}`, nil when it is off.
|
|
36
|
+
def site_identity(_input = nil)
|
|
37
|
+
SiteIdentity.resolve(@context["site"])
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
Liquid::Template.register_filter(Datalog::SiteIdentityFilters)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "authors"
|
|
4
|
+
require_relative "i18n"
|
|
5
|
+
require_relative "image_optimizer"
|
|
6
|
+
require_relative "../lib/datalog/social_cards"
|
|
7
|
+
|
|
8
|
+
# Draws the share cards after every other generator, so the pages that
|
|
9
|
+
# generators add are there, and the series and authors are resolved.
|
|
10
|
+
class Datalog::SocialCardsGenerator < Jekyll::Generator
|
|
11
|
+
safe true
|
|
12
|
+
priority :lowest
|
|
13
|
+
|
|
14
|
+
def generate(site)
|
|
15
|
+
Datalog::SocialCards.generate(site)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
// The academic dashboard (_includes/components/academic-dashboard.html)
|
|
2
|
-
//
|
|
1
|
+
// The academic dashboard (_includes/components/academic-dashboard.html), which
|
|
2
|
+
// the demo renders at /academic/: profiles, citation metrics, the bibliography
|
|
3
|
+
// export, and the submission, calendar and funding lists.
|
|
3
4
|
// _components.scss loads this only when features.academic_dashboard is on.
|
|
4
5
|
|
|
5
6
|
@use 'sass:map';
|
|
@@ -13,21 +14,6 @@
|
|
|
13
14
|
gap: 2.5rem;
|
|
14
15
|
}
|
|
15
16
|
|
|
16
|
-
.academic-dashboard__header {
|
|
17
|
-
text-align: center;
|
|
18
|
-
max-width: 60ch;
|
|
19
|
-
margin: 0 auto;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
.academic-dashboard__header h1 {
|
|
23
|
-
font-size: clamp(1.8rem, 3vw, 2.4rem);
|
|
24
|
-
margin-bottom: 0.5rem;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
.academic-dashboard__header p {
|
|
28
|
-
color: var(--muted-foreground, #4a5568);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
17
|
.academic-dashboard__profiles {
|
|
32
18
|
background: var(--surface-card, #fff);
|
|
33
19
|
border-radius: 1.25rem;
|
|
@@ -47,10 +33,10 @@
|
|
|
47
33
|
}
|
|
48
34
|
|
|
49
35
|
.academic-profile {
|
|
50
|
-
border: 1px solid
|
|
36
|
+
border: 1px solid var(--color-border);
|
|
51
37
|
border-radius: 1rem;
|
|
52
38
|
padding: 1.5rem;
|
|
53
|
-
background:
|
|
39
|
+
background: var(--surface-elevated);
|
|
54
40
|
display: grid;
|
|
55
41
|
gap: 1rem;
|
|
56
42
|
}
|
|
@@ -69,7 +55,7 @@
|
|
|
69
55
|
.academic-profile a {
|
|
70
56
|
font-size: 0.875rem;
|
|
71
57
|
font-weight: 500;
|
|
72
|
-
color: var(--accent
|
|
58
|
+
color: var(--accent);
|
|
73
59
|
}
|
|
74
60
|
|
|
75
61
|
.academic-profile dl {
|
|
@@ -97,12 +83,13 @@
|
|
|
97
83
|
gap: 1.5rem;
|
|
98
84
|
border-radius: 1.25rem;
|
|
99
85
|
padding: 2rem;
|
|
100
|
-
background: rgba(
|
|
86
|
+
background: rgba($color-accent-primary, 0.08);
|
|
101
87
|
}
|
|
102
88
|
|
|
103
89
|
.citation-metrics__totals {
|
|
104
90
|
display: grid;
|
|
105
91
|
gap: 1rem;
|
|
92
|
+
margin: 0;
|
|
106
93
|
}
|
|
107
94
|
|
|
108
95
|
@media (min-width: 40rem) {
|
|
@@ -112,7 +99,7 @@
|
|
|
112
99
|
}
|
|
113
100
|
|
|
114
101
|
.citation-metrics .metric {
|
|
115
|
-
background:
|
|
102
|
+
background: var(--surface-card);
|
|
116
103
|
border-radius: 1rem;
|
|
117
104
|
padding: 1rem;
|
|
118
105
|
text-align: center;
|
|
@@ -133,7 +120,7 @@
|
|
|
133
120
|
}
|
|
134
121
|
|
|
135
122
|
.citation-metrics__details {
|
|
136
|
-
background:
|
|
123
|
+
background: var(--surface-card);
|
|
137
124
|
border-radius: 1rem;
|
|
138
125
|
padding: 1rem 1.5rem;
|
|
139
126
|
}
|
|
@@ -164,7 +151,7 @@
|
|
|
164
151
|
gap: 1rem;
|
|
165
152
|
padding: 0.5rem 0.75rem;
|
|
166
153
|
border-radius: 0.75rem;
|
|
167
|
-
background: linear-gradient(90deg, rgba(
|
|
154
|
+
background: linear-gradient(90deg, rgba($color-accent-primary, 0.15) var(--timeline-value, 0%), transparent 0);
|
|
168
155
|
}
|
|
169
156
|
|
|
170
157
|
.bibliography-export {
|
|
@@ -177,9 +164,9 @@
|
|
|
177
164
|
min-height: 16rem;
|
|
178
165
|
font-family: var(--font-mono, "Fira Code", monospace);
|
|
179
166
|
border-radius: 0.75rem;
|
|
180
|
-
border: 1px solid
|
|
167
|
+
border: 1px solid var(--color-border);
|
|
181
168
|
padding: 1.25rem;
|
|
182
|
-
background:
|
|
169
|
+
background: var(--surface-elevated);
|
|
183
170
|
}
|
|
184
171
|
|
|
185
172
|
.bibliography-export__actions {
|
|
@@ -221,10 +208,10 @@
|
|
|
221
208
|
.opportunity-list li,
|
|
222
209
|
.mentorship-list li,
|
|
223
210
|
.workflow-list li {
|
|
224
|
-
border: 1px solid
|
|
211
|
+
border: 1px solid var(--color-border);
|
|
225
212
|
border-radius: 1rem;
|
|
226
213
|
padding: 1.25rem;
|
|
227
|
-
background:
|
|
214
|
+
background: var(--surface-card);
|
|
228
215
|
}
|
|
229
216
|
|
|
230
217
|
.status-badge {
|
|
@@ -233,8 +220,8 @@
|
|
|
233
220
|
gap: 0.4rem;
|
|
234
221
|
padding: 0.2rem 0.65rem;
|
|
235
222
|
border-radius: 999px;
|
|
236
|
-
background: rgba(
|
|
237
|
-
color: var(--accent-
|
|
223
|
+
background: rgba($color-accent-primary, 0.15);
|
|
224
|
+
color: var(--color-accent-text);
|
|
238
225
|
font-size: 0.85rem;
|
|
239
226
|
font-weight: 600;
|
|
240
227
|
}
|
|
@@ -255,6 +242,7 @@
|
|
|
255
242
|
gap: 1.25rem;
|
|
256
243
|
}
|
|
257
244
|
|
|
245
|
+
|
|
258
246
|
@media (min-width: 48rem) {
|
|
259
247
|
.networking-grid {
|
|
260
248
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
data/_sass/_base.scss
CHANGED
|
@@ -23,6 +23,7 @@ html {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
body {
|
|
26
|
+
color-scheme: light;
|
|
26
27
|
margin: 0;
|
|
27
28
|
font-family: $font-family-sans;
|
|
28
29
|
color: $color-ink-900;
|
|
@@ -33,6 +34,7 @@ body {
|
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
body.dark-mode {
|
|
37
|
+
color-scheme: dark;
|
|
36
38
|
color: $dark-ink-900;
|
|
37
39
|
background-color: $dark-surface-100;
|
|
38
40
|
}
|
data/_sass/_components.scss
CHANGED
|
@@ -143,11 +143,11 @@ body.dark-mode .btn--ghost:hover {
|
|
|
143
143
|
transition: all 0.2s ease;
|
|
144
144
|
|
|
145
145
|
&--primary {
|
|
146
|
-
background: var(--color-accent
|
|
147
|
-
color: white;
|
|
146
|
+
background: var(--color-accent);
|
|
147
|
+
color: var(--accent-foreground, white);
|
|
148
148
|
|
|
149
149
|
&:hover {
|
|
150
|
-
background: var(--color-accent-dark
|
|
150
|
+
background: var(--color-accent-dark);
|
|
151
151
|
}
|
|
152
152
|
}
|
|
153
153
|
|
|
@@ -968,11 +968,15 @@ body.dark-mode {
|
|
|
968
968
|
box-shadow: 0 20px 48px rgba($dark-ink-900, 0.32);
|
|
969
969
|
}
|
|
970
970
|
|
|
971
|
+
.notebook-article__label {
|
|
972
|
+
color: $dark-accent-primary;
|
|
973
|
+
}
|
|
974
|
+
|
|
971
975
|
.notebook-article__summary,
|
|
972
976
|
.notebook-article__meta,
|
|
973
977
|
.notebook-sidebar__metadata dd,
|
|
974
978
|
.notebook-sidebar__stats li {
|
|
975
|
-
color:
|
|
979
|
+
color: $dark-ink-500;
|
|
976
980
|
}
|
|
977
981
|
|
|
978
982
|
.notebook-article__meta span,
|
|
@@ -986,34 +990,28 @@ body.dark-mode {
|
|
|
986
990
|
}
|
|
987
991
|
|
|
988
992
|
.notebook-cell {
|
|
989
|
-
background: rgba($dark-accent-primary, 0.
|
|
993
|
+
background: rgba($dark-accent-primary, 0.22);
|
|
990
994
|
box-shadow: inset 0 0 0 1px rgba($dark-ink-900, 0.35);
|
|
991
995
|
}
|
|
992
996
|
|
|
997
|
+
// Code and outputs sit a shade darker than their cell, as in the light
|
|
998
|
+
// theme. $dark-ink-900 is the lightest ink, so it lightened them instead.
|
|
993
999
|
.notebook-cell__code {
|
|
994
|
-
background: rgba($dark-
|
|
1000
|
+
background: rgba($dark-surface-100, 0.45);
|
|
995
1001
|
}
|
|
996
1002
|
|
|
997
1003
|
.notebook-output {
|
|
998
|
-
background: rgba($dark-
|
|
1004
|
+
background: rgba($dark-surface-100, 0.35);
|
|
999
1005
|
color: $dark-ink-050;
|
|
1000
1006
|
}
|
|
1001
1007
|
|
|
1002
1008
|
.notebook-output__warning {
|
|
1003
|
-
color:
|
|
1004
|
-
}
|
|
1005
|
-
|
|
1006
|
-
.notebook-output--widget {
|
|
1007
|
-
color: $dark-accent-secondary;
|
|
1009
|
+
color: $dark-ink-300;
|
|
1008
1010
|
}
|
|
1009
1011
|
|
|
1010
1012
|
.notebook-output-plotly,
|
|
1011
1013
|
.notebook-output-vega {
|
|
1012
|
-
background: rgba($dark-
|
|
1013
|
-
}
|
|
1014
|
-
|
|
1015
|
-
.notebook-cell {
|
|
1016
|
-
background: rgba($dark-accent-primary, 0.22);
|
|
1014
|
+
background: rgba($dark-surface-100, 0.32);
|
|
1017
1015
|
}
|
|
1018
1016
|
|
|
1019
1017
|
.reference-list li::before,
|
|
@@ -1112,7 +1110,7 @@ body.dark-mode {
|
|
|
1112
1110
|
|
|
1113
1111
|
div {
|
|
1114
1112
|
padding: 0.75rem;
|
|
1115
|
-
background: rgba(
|
|
1113
|
+
background: rgba($color-accent-primary, 0.08);
|
|
1116
1114
|
border-radius: 0.75rem;
|
|
1117
1115
|
text-align: center;
|
|
1118
1116
|
}
|
|
@@ -1134,7 +1132,7 @@ body.dark-mode {
|
|
|
1134
1132
|
|
|
1135
1133
|
body.dark-mode & {
|
|
1136
1134
|
div {
|
|
1137
|
-
background: rgba(
|
|
1135
|
+
background: rgba($dark-accent-primary, 0.2);
|
|
1138
1136
|
}
|
|
1139
1137
|
|
|
1140
1138
|
dt {
|
|
@@ -1168,7 +1166,7 @@ body.dark-mode {
|
|
|
1168
1166
|
align-items: center;
|
|
1169
1167
|
padding: 0.25rem 0.75rem;
|
|
1170
1168
|
border-radius: 999px;
|
|
1171
|
-
background: rgba(
|
|
1169
|
+
background: rgba($color-accent-primary, 0.1);
|
|
1172
1170
|
color: inherit;
|
|
1173
1171
|
text-decoration: none;
|
|
1174
1172
|
font-size: 0.8125rem;
|
|
@@ -1176,16 +1174,16 @@ body.dark-mode {
|
|
|
1176
1174
|
|
|
1177
1175
|
a:hover,
|
|
1178
1176
|
a:focus-visible {
|
|
1179
|
-
background: rgba(
|
|
1177
|
+
background: rgba($color-accent-primary, 0.2);
|
|
1180
1178
|
}
|
|
1181
1179
|
|
|
1182
1180
|
body.dark-mode & a {
|
|
1183
|
-
background: rgba(
|
|
1181
|
+
background: rgba($dark-accent-primary, 0.25);
|
|
1184
1182
|
}
|
|
1185
1183
|
|
|
1186
1184
|
body.dark-mode & a:hover,
|
|
1187
1185
|
body.dark-mode & a:focus-visible {
|
|
1188
|
-
background: rgba(
|
|
1186
|
+
background: rgba($dark-accent-primary, 0.35);
|
|
1189
1187
|
}
|
|
1190
1188
|
}
|
|
1191
1189
|
|