datalog-theme 0.9.0 → 0.9.2
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 +30 -0
- data/CITATION.cff +2 -2
- data/_data/i18n/en.yml +24 -0
- data/_data/i18n/es.yml +24 -0
- data/_data/i18n/pt.yml +24 -0
- data/_includes/components/academic-dashboard.html +4 -4
- data/_includes/components/author-list.html +1 -1
- data/_includes/components/breadcrumbs.html +6 -4
- data/_includes/components/citation-tools.html +5 -53
- data/_includes/components/comments-thread.html +1 -1
- data/_includes/components/contact-form.html +1 -1
- data/_includes/components/correction-report.html +1 -1
- data/_includes/components/hero.html +1 -1
- data/_includes/components/moderation-inbox.html +1 -1
- data/_includes/components/post-hero.html +2 -2
- data/_includes/components/reactions.html +1 -1
- data/_includes/components/reading-list.html +3 -0
- data/_includes/components/reading-mode-toggle.html +2 -0
- data/_includes/components/reading-state-panel.html +0 -9
- data/_includes/components/reading-state-resume.html +17 -0
- data/_includes/components/subscribe-form.html +1 -1
- data/_includes/components/subscription-manage.html +2 -1
- data/_includes/components/visualization-card.html +1 -1
- data/_includes/components/webmentions.html +1 -1
- data/_includes/footer/nav-column.html +1 -1
- data/_includes/footer.html +2 -2
- data/_includes/head.html +13 -12
- data/_includes/header/navigation.html +2 -2
- data/_includes/header.html +1 -1
- data/_includes/layouts/default/article.html +2 -2
- data/_includes/meta/math-config.html +3 -0
- data/_includes/meta/person-json.html +7 -7
- data/_includes/meta/publisher.html +1 -1
- data/_includes/meta/schema.html +32 -30
- data/_includes/meta/scholarly.html +4 -3
- data/_includes/post/related-posts.html +1 -1
- data/_layouts/dataset.html +1 -1
- data/_layouts/home.html +2 -2
- data/_layouts/notebook.html +1 -1
- data/_layouts/package.html +5 -5
- data/_layouts/portfolio.html +1 -1
- data/_layouts/post.html +12 -7
- data/_layouts/project.html +3 -3
- data/_layouts/research.html +7 -67
- data/_plugins/authors.rb +22 -4
- data/_plugins/citation_exports.rb +93 -0
- data/_plugins/image_optimizer.rb +24 -16
- data/_plugins/licenses.rb +15 -3
- data/_plugins/math_preprocessor.rb +65 -3
- data/_plugins/references.rb +30 -8
- data/_plugins/reproducibility.rb +20 -3
- data/_plugins/responsive_content.rb +40 -0
- data/_plugins/revisions.rb +1 -0
- data/_plugins/scholarly.rb +5 -0
- data/_plugins/statements.rb +2 -2
- data/_sass/_base.scss +5 -0
- data/_sass/_layout.scss +18 -1
- data/_sass/_mathematical.scss +11 -1
- data/_sass/_print.scss +16 -8
- data/_sass/_syntax-highlighting.scss +1 -1
- data/assets/js/dist/chunks/chunk-VZ5WKQVA.js +1 -0
- data/assets/js/dist/chunks/chunk-WIRUK3OZ.js +1 -0
- data/assets/js/dist/comments.js +2 -2
- data/assets/js/dist/contact.js +1 -1
- data/assets/js/dist/corrections.js +1 -1
- data/assets/js/dist/moderation.js +1 -1
- data/assets/js/dist/reactions.js +1 -1
- data/assets/js/dist/reading-state.js +1 -1
- data/assets/js/dist/sources.json +16 -16
- data/assets/js/dist/subscriptions.js +1 -1
- data/assets/js/dist/visualizations.js +2 -2
- data/assets/js/dist/webmentions.js +1 -1
- data/lib/datalog/theme/version.rb +1 -1
- metadata +7 -4
- data/assets/js/dist/chunks/chunk-2DYDWUFX.js +0 -1
- data/assets/js/dist/chunks/chunk-V7734B2G.js +0 -1
data/_layouts/research.html
CHANGED
|
@@ -38,7 +38,7 @@ schema_type: ScholarlyArticle
|
|
|
38
38
|
{% assign show_publications = page.show_publications | default: false %}
|
|
39
39
|
|
|
40
40
|
<div class="research-wrapper" itemscope itemtype="https://schema.org/ScholarlyArticle">
|
|
41
|
-
<meta itemprop="headline" content="{{ page.title }}" />
|
|
41
|
+
<meta itemprop="headline" content="{{ page.title | escape }}" />
|
|
42
42
|
{% if page.date %}
|
|
43
43
|
<meta itemprop="datePublished" content="{{ page.date | date_to_xmlschema }}" />
|
|
44
44
|
{% endif %}
|
|
@@ -56,7 +56,7 @@ schema_type: ScholarlyArticle
|
|
|
56
56
|
{% endif %}
|
|
57
57
|
|
|
58
58
|
<header class="research-header" aria-labelledby="research-title">
|
|
59
|
-
<h1 id="research-title" class="research-title">{{ page.title }}</h1>
|
|
59
|
+
<h1 id="research-title" class="research-title">{{ page.title | escape }}</h1>
|
|
60
60
|
|
|
61
61
|
<div class="research-authors" aria-label="Authors">
|
|
62
62
|
<h2 class="visually-hidden">Authors</h2>
|
|
@@ -207,67 +207,7 @@ schema_type: ScholarlyArticle
|
|
|
207
207
|
</section>
|
|
208
208
|
{% endif %}
|
|
209
209
|
|
|
210
|
-
|
|
211
|
-
<h2 id="citation-exports-heading">Citation exports</h2>
|
|
212
|
-
{% capture bibtex_authors %}
|
|
213
|
-
{% for author in authors %}
|
|
214
|
-
{% assign author_name = author.name | default: author %}
|
|
215
|
-
{{ author_name | strip }}{% unless forloop.last %} and {% endunless %}
|
|
216
|
-
{% endfor %}
|
|
217
|
-
{% endcapture %}
|
|
218
|
-
{% capture bibtex_entry %}
|
|
219
|
-
@article{ {{ citation_key }},
|
|
220
|
-
title = { {{ page.title | strip }} },
|
|
221
|
-
author = { {{ bibtex_authors | strip }} },
|
|
222
|
-
{% if publication_name %}journal = { {{ publication_name | strip }} },{% endif %}
|
|
223
|
-
{% if page.date %}year = { {{ page.date | date: '%Y' }} },{% endif %}
|
|
224
|
-
{% if doi %}doi = { {{ doi }} },{% endif %}
|
|
225
|
-
{% if page.publisher %}publisher = { {{ page.publisher }} },{% endif %}
|
|
226
|
-
{% if page.volume %}volume = { {{ page.volume }} },{% endif %}
|
|
227
|
-
{% if page.number %}number = { {{ page.number }} },{% endif %}
|
|
228
|
-
{% if page.pages %}pages = { {{ page.pages }} },{% endif %}
|
|
229
|
-
}
|
|
230
|
-
{% endcapture %}
|
|
231
|
-
{% capture ris_entry %}
|
|
232
|
-
TY - JOUR
|
|
233
|
-
TI - {{ page.title | strip }}
|
|
234
|
-
{% if publication_name %}JO - {{ publication_name | strip }}
|
|
235
|
-
{% endif %}
|
|
236
|
-
{% if page.date %}PY - {{ page.date | date: '%Y/%m/%d' }}
|
|
237
|
-
{% endif %}
|
|
238
|
-
{% for author in authors %}{% assign author_name = author.name | default: author %}AU - {{ author_name | strip }}
|
|
239
|
-
{% endfor %}
|
|
240
|
-
{% if doi %}DO - {{ doi }}
|
|
241
|
-
{% endif %}
|
|
242
|
-
ER -
|
|
243
|
-
{% endcapture %}
|
|
244
|
-
{% capture endnote_entry %}
|
|
245
|
-
%0 Journal Article
|
|
246
|
-
%T {{ page.title | strip }}
|
|
247
|
-
{% for author in authors %}{% assign author_name = author.name | default: author %}%A {{ author_name | strip }}
|
|
248
|
-
{% endfor %}
|
|
249
|
-
{% if publication_name %}%J {{ publication_name | strip }}
|
|
250
|
-
{% endif %}
|
|
251
|
-
{% if page.date %}%D {{ page.date | date: '%Y' }}
|
|
252
|
-
{% endif %}
|
|
253
|
-
{% if doi %}%R {{ doi }}
|
|
254
|
-
{% endif %}
|
|
255
|
-
{% endcapture %}
|
|
256
|
-
<div class="citation-export-grid">
|
|
257
|
-
<article>
|
|
258
|
-
<h3>BibTeX</h3>
|
|
259
|
-
<textarea readonly aria-label="BibTeX citation">{{ bibtex_entry | strip }}</textarea>
|
|
260
|
-
</article>
|
|
261
|
-
<article>
|
|
262
|
-
<h3>RIS</h3>
|
|
263
|
-
<textarea readonly aria-label="RIS citation">{{ ris_entry | strip }}</textarea>
|
|
264
|
-
</article>
|
|
265
|
-
<article>
|
|
266
|
-
<h3>EndNote</h3>
|
|
267
|
-
<textarea readonly aria-label="EndNote citation">{{ endnote_entry | strip }}</textarea>
|
|
268
|
-
</article>
|
|
269
|
-
</div>
|
|
270
|
-
</section>
|
|
210
|
+
{% include components/citation-tools.html authors=authors journal=publication_name %}
|
|
271
211
|
|
|
272
212
|
<section class="research-body" itemprop="articleBody">
|
|
273
213
|
{% if methodology_content %}
|
|
@@ -347,7 +287,7 @@ ER -
|
|
|
347
287
|
<li>
|
|
348
288
|
<article class="appendix-item">
|
|
349
289
|
{% if appendix.title %}
|
|
350
|
-
<h3>{{ appendix.title }}</h3>
|
|
290
|
+
<h3>{{ appendix.title | escape }}</h3>
|
|
351
291
|
{% endif %}
|
|
352
292
|
{% if appendix.content %}
|
|
353
293
|
{{ appendix.content | markdownify }}
|
|
@@ -393,7 +333,7 @@ ER -
|
|
|
393
333
|
{% for publication in group_items %}
|
|
394
334
|
<li class="publication-list__item">
|
|
395
335
|
<article>
|
|
396
|
-
<h4 class="publication-title">{{ publication.title }}</h4>
|
|
336
|
+
<h4 class="publication-title">{{ publication.title | escape }}</h4>
|
|
397
337
|
{% assign publication_authors = publication.authors | arrayify %}
|
|
398
338
|
{% if publication_authors and publication_authors.size > 0 %}
|
|
399
339
|
<p class="publication-authors">{{ publication_authors | join: ', ' }}</p>
|
|
@@ -416,7 +356,7 @@ ER -
|
|
|
416
356
|
{% for publication in publication_entries %}
|
|
417
357
|
<li class="publication-list__item">
|
|
418
358
|
<article>
|
|
419
|
-
<h4 class="publication-title">{{ publication.title }}</h4>
|
|
359
|
+
<h4 class="publication-title">{{ publication.title | escape }}</h4>
|
|
420
360
|
{% assign publication_authors = publication.authors | arrayify %}
|
|
421
361
|
{% if publication_authors and publication_authors.size > 0 %}
|
|
422
362
|
<p class="publication-authors">{{ publication_authors | join: ', ' }}</p>
|
|
@@ -456,7 +396,7 @@ ER -
|
|
|
456
396
|
<li>
|
|
457
397
|
<article class="version-entry">
|
|
458
398
|
<header>
|
|
459
|
-
<strong>{{ version.version | default: version.title | default: version }}</strong>
|
|
399
|
+
<strong>{{ version.version | default: version.title | default: version | escape }}</strong>
|
|
460
400
|
{% if version.date %}<span class="version-date">{{ version.date | localize_date: date_format }}</span>{% endif %}
|
|
461
401
|
</header>
|
|
462
402
|
{% if version.notes %}
|
data/_plugins/authors.rb
CHANGED
|
@@ -27,13 +27,13 @@ module Datalog
|
|
|
27
27
|
def authors(page, site)
|
|
28
28
|
entries = entries(value(page, "authors"))
|
|
29
29
|
entries = entries(value(page, "author")) if entries.empty?
|
|
30
|
-
records = entries.filter_map { |entry| resolve(entry, site) }
|
|
30
|
+
records = entries.filter_map { |entry| resolve(entry, site, page) }
|
|
31
31
|
records = [site_author(site)].compact if records.empty?
|
|
32
32
|
apply_page_affiliation(records, page)
|
|
33
33
|
end
|
|
34
34
|
|
|
35
35
|
def contributors(page, site)
|
|
36
|
-
entries(value(page, "contributors")).filter_map { |entry| resolve(entry, site) }
|
|
36
|
+
entries(value(page, "contributors")).filter_map { |entry| resolve(entry, site, page) }
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
# A list, one entry, or names in a string separated by semicolons.
|
|
@@ -46,9 +46,15 @@ module Datalog
|
|
|
46
46
|
end
|
|
47
47
|
|
|
48
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)
|
|
49
|
+
def resolve(entry, site, page = {})
|
|
50
50
|
own = entry.is_a?(Hash) ? present(entry) : {}
|
|
51
|
-
key = own["id"] || own["name"] || entry.
|
|
51
|
+
key = own["id"] || own["name"] || (entry if entry.is_a?(String) || entry.is_a?(Symbol))
|
|
52
|
+
unless (key.is_a?(String) || key.is_a?(Symbol)) && !key.to_s.strip.empty?
|
|
53
|
+
path = value(page, "path")
|
|
54
|
+
raise Jekyll::Errors::FatalException,
|
|
55
|
+
"#{path} author entry #{entry.inspect} needs a name, an author id, or a map with name or id"
|
|
56
|
+
end
|
|
57
|
+
key = key.to_s.strip
|
|
52
58
|
record = known(key, site).merge(own)
|
|
53
59
|
record["name"] ||= key
|
|
54
60
|
return if record["name"].to_s.strip.empty?
|
|
@@ -63,6 +69,11 @@ module Datalog
|
|
|
63
69
|
record = value(data, key.to_s)
|
|
64
70
|
return present(record) if record.is_a?(Hash)
|
|
65
71
|
|
|
72
|
+
if data.is_a?(Hash)
|
|
73
|
+
record = data.values.find { |candidate| candidate.is_a?(Hash) && candidate["name"] == key }
|
|
74
|
+
return present(record) if record
|
|
75
|
+
end
|
|
76
|
+
|
|
66
77
|
author = site_author_profile(site)
|
|
67
78
|
author && author["name"] == key ? author : {}
|
|
68
79
|
end
|
|
@@ -85,6 +96,13 @@ module Datalog
|
|
|
85
96
|
record["image"] ||= record["avatar"] || record["photo"]
|
|
86
97
|
record["orcid"] = "https://orcid.org/#{record['orcid']}" if record["orcid"].to_s.match?(ORCID_ID)
|
|
87
98
|
record["site_author"] = record["name"] == site_author_profile(site)&.fetch("name")
|
|
99
|
+
PROFILE_URLS.each do |key, template|
|
|
100
|
+
next unless record[key]
|
|
101
|
+
|
|
102
|
+
prefix = template.delete_suffix("%s").delete_prefix("https://")
|
|
103
|
+
record[key] = record[key].to_s.strip.sub(%r{\Ahttps?://(?:www\.)?#{Regexp.escape(prefix)}}i, "")
|
|
104
|
+
.sub(/\A@/, "").chomp("/")
|
|
105
|
+
end
|
|
88
106
|
record["same_as"] = profiles(record)
|
|
89
107
|
record
|
|
90
108
|
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Datalog
|
|
4
|
+
module CitationExports
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
TYPES = {
|
|
8
|
+
"article" => ["article", "JOUR", "Journal Article"],
|
|
9
|
+
"journal" => ["article", "JOUR", "Journal Article"],
|
|
10
|
+
"blog" => %w[misc BLOG Blog],
|
|
11
|
+
"dataset" => %w[misc DATA Dataset],
|
|
12
|
+
"software" => ["misc", "COMP", "Computer Program"],
|
|
13
|
+
"book" => %w[book BOOK Book],
|
|
14
|
+
"inproceedings" => ["inproceedings", "CPAPER", "Conference Paper"],
|
|
15
|
+
"misc" => %w[misc GEN Generic]
|
|
16
|
+
}.freeze
|
|
17
|
+
|
|
18
|
+
def text(value)
|
|
19
|
+
value.to_s.gsub(/\s+/, " ").strip
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def bibtex(value)
|
|
23
|
+
text(value).gsub(/[\\&%$#_{}~^]/) do |character|
|
|
24
|
+
{ "\\" => '\\textbackslash{}', "~" => '\\textasciitilde{}', "^" => '\\textasciicircum{}' }
|
|
25
|
+
.fetch(character) { "\\#{character}" }
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def render(record, authors, kind, key)
|
|
30
|
+
bib_type, ris_type, endnote_type = TYPES.fetch(kind.to_s.downcase, TYPES["misc"])
|
|
31
|
+
names = authors.map { |author| text(Authors.value(author, "name") || author) }
|
|
32
|
+
bib_names = authors.each_with_index.map do |author, index|
|
|
33
|
+
name = bibtex(names[index])
|
|
34
|
+
Authors.value(author, "type") == "Organization" ? "{#{name}}" : name
|
|
35
|
+
end
|
|
36
|
+
fields = { "title" => bibtex(record["title"]), "author" => bib_names.join(" and ") }
|
|
37
|
+
record.each { |field, value| fields[field] = bibtex(value) unless field == "title" || value.to_s.empty? }
|
|
38
|
+
body = fields.map { |field, value| " #{field} = { #{value} }" }.join(",\n")
|
|
39
|
+
bib = "@#{bib_type}{ #{key},\n#{body}\n}"
|
|
40
|
+
|
|
41
|
+
{ "bibtex" => bib, "ris" => ris(record, names, ris_type), "endnote" => endnote(record, names, endnote_type) }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def ris(record, names, type)
|
|
45
|
+
lines = ["TY - #{type}", "TI - #{text(record['title'])}"]
|
|
46
|
+
lines.concat(names.map { |name| "AU - #{name}" })
|
|
47
|
+
{ "year" => "PY", "publisher" => "PB", "journal" => "JO", "doi" => "DO", "url" => "UR",
|
|
48
|
+
"volume" => "VL", "number" => "IS" }.each do |field, tag|
|
|
49
|
+
lines << "#{tag} - #{text(record[field])}" unless record[field].to_s.empty?
|
|
50
|
+
end
|
|
51
|
+
unless record["pages"].to_s.empty?
|
|
52
|
+
first, last = text(record["pages"]).split(/[-–—]+/, 2)
|
|
53
|
+
lines << "SP - #{first}"
|
|
54
|
+
lines << "EP - #{last}" if last
|
|
55
|
+
end
|
|
56
|
+
(lines << "ER -").join("\n")
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def endnote(record, names, type)
|
|
60
|
+
lines = ["%0 #{type}", "%T #{text(record['title'])}"]
|
|
61
|
+
lines.concat(names.map { |name| "%A #{name}" })
|
|
62
|
+
{ "year" => "D", "publisher" => "I", "journal" => "J", "doi" => "R", "url" => "U",
|
|
63
|
+
"volume" => "V", "number" => "N", "pages" => "P" }.each do |field, tag|
|
|
64
|
+
lines << "%#{tag} #{text(record[field])}" unless record[field].to_s.empty?
|
|
65
|
+
end
|
|
66
|
+
lines.join("\n")
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
module CitationExportFilters
|
|
71
|
+
def citation_exports(page, options = {})
|
|
72
|
+
site = @context["site"]
|
|
73
|
+
get = ->(key) { Authors.value(options, key) || Authors.value(page, key) }
|
|
74
|
+
title = get.call("title") || Authors.value(site, "title")
|
|
75
|
+
authors = Authors.value(options, "authors") || Authors.authors(page, site)
|
|
76
|
+
authors = Authors.entries(authors)
|
|
77
|
+
publisher = get.call("publisher") || Authors.value(Authors.value(site, "publisher"), "name") ||
|
|
78
|
+
Authors.value(site, "title")
|
|
79
|
+
key = slugify(get.call("citation_key") || Authors.value(page, "slug") || title)
|
|
80
|
+
kind = get.call("citation_type") || Authors.value(options, "type") || "article"
|
|
81
|
+
record = {
|
|
82
|
+
"title" => title, "year" => (date(get.call("date"), "%Y") if get.call("date")),
|
|
83
|
+
"publisher" => publisher, "journal" => get.call("journal") || get.call("publication"),
|
|
84
|
+
"doi" => get.call("doi"), "url" => absolute_url(get.call("url") || "/"),
|
|
85
|
+
"volume" => get.call("volume"), "number" => get.call("issue") || get.call("number"),
|
|
86
|
+
"pages" => get.call("pages")
|
|
87
|
+
}
|
|
88
|
+
CitationExports.render(record, authors, kind, key)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
Liquid::Template.register_filter(Datalog::CitationExportFilters)
|
data/_plugins/image_optimizer.rb
CHANGED
|
@@ -8,6 +8,7 @@ require "nokogiri"
|
|
|
8
8
|
require "open3"
|
|
9
9
|
require "tmpdir"
|
|
10
10
|
require "tempfile"
|
|
11
|
+
require "uri"
|
|
11
12
|
|
|
12
13
|
module Jekyll
|
|
13
14
|
module ImageOptimizer
|
|
@@ -76,18 +77,19 @@ module Jekyll
|
|
|
76
77
|
img["loading"] ||= "lazy"
|
|
77
78
|
end
|
|
78
79
|
img["decoding"] ||= "async"
|
|
79
|
-
|
|
80
80
|
normalized_src = normalize_src(img["src"], site)
|
|
81
81
|
picture_entry = manifest[normalized_src]
|
|
82
82
|
|
|
83
83
|
if picture_entry
|
|
84
84
|
offer_variants(img, picture_entry, image_config, baseurl)
|
|
85
|
-
img["width"]
|
|
86
|
-
|
|
85
|
+
if !img["width"] && !img["height"] && picture_entry["width"] && picture_entry["height"]
|
|
86
|
+
img["width"] = picture_entry["width"].to_s
|
|
87
|
+
img["height"] = picture_entry["height"].to_s
|
|
88
|
+
end
|
|
87
89
|
else
|
|
88
|
-
next if img["width"]
|
|
90
|
+
next if img["width"] || img["height"]
|
|
89
91
|
|
|
90
|
-
source = image_source_path(site,
|
|
92
|
+
source = image_source_path(site, normalized_src)
|
|
91
93
|
next unless source && File.exist?(source)
|
|
92
94
|
|
|
93
95
|
width, height = FastImage.size(source)
|
|
@@ -270,7 +272,7 @@ module Jekyll
|
|
|
270
272
|
relative_path = static_file.relative_path.sub(%r{^/}, "")
|
|
271
273
|
dir = File.dirname(relative_path)
|
|
272
274
|
responsive_dir = dir == "." ? "responsive" : File.join(dir, "responsive")
|
|
273
|
-
base = File.basename(relative_path
|
|
275
|
+
base = File.basename(relative_path)
|
|
274
276
|
Jekyll::ResponsiveImageStaticFile.new(site, responsive_dir, "#{base}-#{target_width}w.#{extension_for(format)}",
|
|
275
277
|
cached)
|
|
276
278
|
end
|
|
@@ -420,13 +422,19 @@ module Jekyll
|
|
|
420
422
|
|
|
421
423
|
def original_variant(src, width, height)
|
|
422
424
|
{
|
|
423
|
-
"url" => src,
|
|
425
|
+
"url" => encode_image_path(src),
|
|
424
426
|
"width" => width,
|
|
425
427
|
"height" => height,
|
|
426
428
|
"format" => normalize_format(File.extname(src))
|
|
427
429
|
}
|
|
428
430
|
end
|
|
429
431
|
|
|
432
|
+
# These are filesystem paths, not already escaped URLs. Encode them once
|
|
433
|
+
# when creating the manifest so both Markdown and the include use valid srcsets.
|
|
434
|
+
def encode_image_path(path)
|
|
435
|
+
path.split("/", -1).map { |part| URI.encode_www_form_component(part).gsub("+", "%20") }.join("/")
|
|
436
|
+
end
|
|
437
|
+
|
|
430
438
|
# Variant URLs are site paths. A site served below a baseurl needs it in
|
|
431
439
|
# front of them, as relative_url adds it in templates.
|
|
432
440
|
def build_srcset(variants, baseurl = "")
|
|
@@ -440,16 +448,16 @@ module Jekyll
|
|
|
440
448
|
end
|
|
441
449
|
|
|
442
450
|
def normalize_src(src, site)
|
|
443
|
-
|
|
444
|
-
|
|
451
|
+
# Relative image URLs belong to the page, not to the source root. Leave
|
|
452
|
+
# them alone rather than offer another image's dimensions or variants.
|
|
453
|
+
return unless src&.start_with?("/") && !src.start_with?("//")
|
|
445
454
|
|
|
446
455
|
baseurl = site&.baseurl.to_s
|
|
447
|
-
normalized = src.
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
normalized
|
|
452
|
-
"/#{normalized}"
|
|
456
|
+
normalized = URI::DEFAULT_PARSER.unescape(src.split(/[?#]/, 2).first)
|
|
457
|
+
if !baseurl.empty? && (normalized == baseurl || normalized.start_with?("#{baseurl.chomp('/')}/"))
|
|
458
|
+
normalized = normalized.delete_prefix(baseurl.chomp("/"))
|
|
459
|
+
end
|
|
460
|
+
normalized
|
|
453
461
|
end
|
|
454
462
|
|
|
455
463
|
def image_config(site)
|
|
@@ -475,7 +483,7 @@ class Jekyll::ResponsiveImageStaticFile < Jekyll::StaticFile
|
|
|
475
483
|
def initialize(site, relative_dir, name, cached_path)
|
|
476
484
|
@cached_path = cached_path
|
|
477
485
|
super(site, site.source, "/#{relative_dir}", name)
|
|
478
|
-
@url = "/#{relative_dir}/#{name}"
|
|
486
|
+
@url = Jekyll::ImageOptimizer.encode_image_path("/#{relative_dir}/#{name}")
|
|
479
487
|
end
|
|
480
488
|
|
|
481
489
|
def path
|
data/_plugins/licenses.rb
CHANGED
|
@@ -76,16 +76,27 @@ module Datalog
|
|
|
76
76
|
# The page's own value, else the site's; `false` declines the site's.
|
|
77
77
|
def setting(page, page_key, site, site_key)
|
|
78
78
|
value = Authors.value(page, page_key)
|
|
79
|
-
|
|
80
|
-
|
|
79
|
+
own_only = OWN_LICENSE.include?(Authors.value(page, "collection").to_s)
|
|
80
|
+
fallback = Authors.value(site, site_key) unless own_only
|
|
81
|
+
return fallback if value.nil? || value == true
|
|
81
82
|
|
|
82
|
-
Authors.value(
|
|
83
|
+
if value.is_a?(Hash) && !Authors.present(value).keys.intersect?(%w[id name url])
|
|
84
|
+
defaults = fallback.is_a?(Hash) ? Authors.present(fallback) : { "name" => fallback }
|
|
85
|
+
return defaults.merge(Authors.present(value))
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
value
|
|
83
89
|
end
|
|
84
90
|
|
|
85
91
|
def resolve(value, page, site)
|
|
86
92
|
return if value.nil? || value == false || (value.is_a?(String) && value.strip.empty?)
|
|
93
|
+
unless value.is_a?(String) || value.is_a?(Hash)
|
|
94
|
+
raise Jekyll::Errors::FatalException, "#{Authors.value(page, 'path')} license must be a name, a map or false"
|
|
95
|
+
end
|
|
87
96
|
|
|
88
97
|
given = value.is_a?(Hash) ? Authors.present(value) : { "name" => value.to_s.strip }
|
|
98
|
+
return unless given.keys.intersect?(%w[id name url])
|
|
99
|
+
|
|
89
100
|
id = LOOKUP[key(given["id"] || given["name"])]
|
|
90
101
|
name, url = KNOWN[id] if id
|
|
91
102
|
# A name that is not itself an identifier is the label the page chose.
|
|
@@ -114,6 +125,7 @@ module Datalog
|
|
|
114
125
|
|
|
115
126
|
text = value.to_s.strip
|
|
116
127
|
return text.to_i if text.match?(/\A\d{4}\z/)
|
|
128
|
+
return text if text.match?(/\A\d{4}[-–]\d{4}\z/)
|
|
117
129
|
|
|
118
130
|
Time.parse(text).year unless text.empty?
|
|
119
131
|
rescue ArgumentError
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "cgi"
|
|
4
|
+
require "kramdown"
|
|
5
|
+
require "kramdown-parser-gfm"
|
|
4
6
|
|
|
5
7
|
module MathPreprocessor
|
|
6
8
|
DISPLAY_PATTERNS = [
|
|
@@ -46,7 +48,7 @@ module MathPreprocessor
|
|
|
46
48
|
# so fenced blocks, highlight tags, <pre>/<code> elements and inline code
|
|
47
49
|
# spans are set aside before looking for math and put back afterwards.
|
|
48
50
|
CODE_PATTERNS = [
|
|
49
|
-
/^
|
|
51
|
+
/^[ \t]*(?<fence>(?<marker>`|~)\k<marker>{2,})[^\n]*\n.*?(?:^[ \t]*\k<fence>\k<marker>*[ \t]*$|\z)/m,
|
|
50
52
|
/\{%-?\s*highlight\b.*?\{%-?\s*endhighlight\s*-?%\}/m,
|
|
51
53
|
%r{<(pre|code)\b[^>]*>.*?</\1>}mi,
|
|
52
54
|
/(?<!`)(`+)(?!`)(?:(?!\n[ \t]*\n).)+?(?<!`)\1(?!`)/m
|
|
@@ -56,6 +58,31 @@ module MathPreprocessor
|
|
|
56
58
|
# an escape: a raw NUL byte in the source stopped RuboCop from parsing the file.
|
|
57
59
|
PLACEHOLDER = /\x00(\d+)\x00/
|
|
58
60
|
|
|
61
|
+
# Indentation alone does not identify code: list continuations and continued
|
|
62
|
+
# paragraphs can have the same indentation. Let the Markdown parser identify
|
|
63
|
+
# the code blocks, retaining their original source lines for masking.
|
|
64
|
+
class IndentedCodeParser < Kramdown::Parser::GFM
|
|
65
|
+
attr_reader :code_ranges
|
|
66
|
+
|
|
67
|
+
def initialize(source, options)
|
|
68
|
+
super
|
|
69
|
+
@code_ranges = []
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def parse_codeblock
|
|
73
|
+
first = @src.current_line_number - 1
|
|
74
|
+
super.tap do |parsed|
|
|
75
|
+
@code_ranges << (first...(@src.current_line_number - 1)) if parsed
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def self.ranges(source)
|
|
80
|
+
parser = new(source, {})
|
|
81
|
+
parser.parse
|
|
82
|
+
parser.code_ranges
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
59
86
|
class Processor
|
|
60
87
|
attr_reader :expressions
|
|
61
88
|
|
|
@@ -68,9 +95,10 @@ module MathPreprocessor
|
|
|
68
95
|
return @content unless @content&.match?(/\$|\\\(|\\\[|\\begin\{/)
|
|
69
96
|
|
|
70
97
|
@segments = []
|
|
71
|
-
processed = CODE_PATTERNS.reduce(@content
|
|
98
|
+
processed = CODE_PATTERNS.reduce(mask_indented_code(@content)) do |text, pattern|
|
|
72
99
|
text.gsub(pattern) { |match| mask(match) }
|
|
73
100
|
end
|
|
101
|
+
processed = normalize_inline_dollars(processed)
|
|
74
102
|
processed = apply_patterns(processed, DISPLAY_PATTERNS, display: true)
|
|
75
103
|
processed = apply_patterns(processed, INLINE_PATTERNS, display: false)
|
|
76
104
|
# A segment set aside can contain the placeholder of an earlier one.
|
|
@@ -85,6 +113,37 @@ module MathPreprocessor
|
|
|
85
113
|
"\x00#{@segments.size - 1}\x00"
|
|
86
114
|
end
|
|
87
115
|
|
|
116
|
+
def mask_indented_code(text)
|
|
117
|
+
return text unless text.match?(/^(?: {4}|\t)/)
|
|
118
|
+
|
|
119
|
+
lines = text.lines
|
|
120
|
+
IndentedCodeParser.ranges(text).reverse_each do |range|
|
|
121
|
+
block = lines[range].join
|
|
122
|
+
# Keep the final line boundary visible to subsequent block patterns.
|
|
123
|
+
ending = block.end_with?("\n") ? "\n" : ""
|
|
124
|
+
lines[range] = [mask(block.delete_suffix(ending)) + ending]
|
|
125
|
+
end
|
|
126
|
+
lines.join
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Kramdown accepts $$...$$ inside prose as inline math. A div there is
|
|
130
|
+
# escaped by Markdown and leaks its attributes into the visible article.
|
|
131
|
+
# Normalize before wrapping; code is already masked and standalone or
|
|
132
|
+
# multiline display equations retain their original delimiters.
|
|
133
|
+
def normalize_inline_dollars(text)
|
|
134
|
+
text.gsub(DISPLAY_PATTERNS.first[:regex]) do |expression|
|
|
135
|
+
match = Regexp.last_match
|
|
136
|
+
body = match[:body]
|
|
137
|
+
next expression if body.include?("\n") || body.strip.empty?
|
|
138
|
+
|
|
139
|
+
before = match.pre_match.split("\n", -1).last.to_s
|
|
140
|
+
after = match.post_match.split("\n", 2).first.to_s
|
|
141
|
+
next expression unless before.match?(/\S/) || after.match?(/\S/)
|
|
142
|
+
|
|
143
|
+
"$#{body.strip}$"
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
88
147
|
def apply_patterns(text, patterns, display: false)
|
|
89
148
|
patterns.reduce(text) do |result, pattern|
|
|
90
149
|
result.gsub(pattern[:regex]) do |match|
|
|
@@ -118,6 +177,9 @@ module MathPreprocessor
|
|
|
118
177
|
"aria-label" => alt_text,
|
|
119
178
|
"tabindex" => "0"
|
|
120
179
|
}
|
|
180
|
+
# Kramdown must not interpret TeX's escaped delimiters or underscores as
|
|
181
|
+
# Markdown inside an inline HTML span.
|
|
182
|
+
attributes["markdown"] = "0" unless display
|
|
121
183
|
|
|
122
184
|
attribute_string = attributes.map do |key, value|
|
|
123
185
|
next if value.nil? || value.strip.empty?
|
|
@@ -125,7 +187,7 @@ module MathPreprocessor
|
|
|
125
187
|
%(#{key}="#{CGI.escapeHTML(value)}")
|
|
126
188
|
end.compact.join(" ")
|
|
127
189
|
|
|
128
|
-
inner = "#{open}#{latex}#{close}"
|
|
190
|
+
inner = CGI.escapeHTML("#{open}#{latex}#{close}")
|
|
129
191
|
"<#{tag} #{attribute_string}>#{inner}</#{tag}>"
|
|
130
192
|
end
|
|
131
193
|
|
data/_plugins/references.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "cgi"
|
|
4
|
+
require "strscan"
|
|
4
5
|
|
|
5
6
|
module Datalog
|
|
6
7
|
# Numbered figures and tables, and references to them, within a page:
|
|
@@ -41,7 +42,8 @@ module Datalog
|
|
|
41
42
|
ID = /\A[A-Za-z][\w.:-]*\z/
|
|
42
43
|
# A label given in place of the number, as in "Theorem A".
|
|
43
44
|
CUSTOM_LABEL = /\A[[:alnum:]][[:alnum:].'*-]*\z/
|
|
44
|
-
TARGET = /data-ref-target="([^"]+)" data-ref-kind="([a-z]+)"(?: data-ref-number="([^"]+)")
|
|
45
|
+
TARGET = /data-ref-target="([^"]+)"[ ]data-ref-kind="([a-z]+)"(?:[ ]data-ref-number="([^"]+)")?
|
|
46
|
+
([ ]data-ref-numbered="true")?/x
|
|
45
47
|
# A label ends with a full stop unless the tag chose another ending.
|
|
46
48
|
LABEL = %r{<span class="datalog-ref-label" data-ref-for="([^"]+)"(?: data-ref-end="([^"]*)")?></span>}
|
|
47
49
|
LINK = %r{<a class="datalog-ref" href="#([^"]+)" data-ref="\1">[^<]*</a>}
|
|
@@ -51,7 +53,12 @@ module Datalog
|
|
|
51
53
|
content = document.content
|
|
52
54
|
return unless content&.include?("data-ref")
|
|
53
55
|
|
|
54
|
-
|
|
56
|
+
# Feeds and listings embed content that has already been numbered on its
|
|
57
|
+
# own page. Only process this document's remaining placeholders.
|
|
58
|
+
targets = targets(content.scan(TARGET).reject { |entry| entry[3] }, document)
|
|
59
|
+
content = content.gsub(TARGET) do |target|
|
|
60
|
+
Regexp.last_match(4) ? target : %(#{target} data-ref-numbered="true")
|
|
61
|
+
end
|
|
55
62
|
content = content.gsub(LABEL) do
|
|
56
63
|
id, ending = Regexp.last_match.captures
|
|
57
64
|
text = "#{CGI.escapeHTML(targets.fetch(id))}#{ending || '.'}"
|
|
@@ -113,7 +120,8 @@ module Datalog
|
|
|
113
120
|
|
|
114
121
|
content.gsub(LINK) do
|
|
115
122
|
id = Regexp.last_match(1)
|
|
116
|
-
|
|
123
|
+
label = CGI.escapeHTML(targets[id])
|
|
124
|
+
%(<a class="datalog-ref" href="##{id}" data-ref="#{id}" data-ref-numbered="true">#{label}</a>)
|
|
117
125
|
end
|
|
118
126
|
end
|
|
119
127
|
|
|
@@ -140,10 +148,24 @@ module Datalog
|
|
|
140
148
|
end
|
|
141
149
|
|
|
142
150
|
# key="value", key='value' or key=variable.
|
|
143
|
-
def attributes(markup, context)
|
|
144
|
-
|
|
145
|
-
|
|
151
|
+
def attributes(markup, context, tag = "reference")
|
|
152
|
+
scanner = StringScanner.new(markup)
|
|
153
|
+
result = {}
|
|
154
|
+
until scanner.eos?
|
|
155
|
+
scanner.skip(/\s+/)
|
|
156
|
+
break if scanner.eos?
|
|
157
|
+
|
|
158
|
+
unless scanner.scan(/(\w+)\s*=\s*(?:"((?:[^"\\]|\\.)*)"|'((?:[^'\\]|\\.)*)'|([\w.\[\]-]+))(?=\s|\z)/m)
|
|
159
|
+
page = context.registers[:page] || {}
|
|
160
|
+
raise Liquid::ArgumentError,
|
|
161
|
+
"#{page['path'] || page['url']} {% #{tag} %} has invalid attributes near #{scanner.rest.inspect}"
|
|
162
|
+
end
|
|
163
|
+
# Ruby 3.2's StringScanner#captures returns "" for unmatched groups.
|
|
164
|
+
# values_at preserves nil so quoted literals stay distinct from variables.
|
|
165
|
+
key, double, single, variable = scanner.values_at(1, 2, 3, 4)
|
|
166
|
+
result[key] = variable ? context[variable].to_s : (double || single).gsub(/\\([\\"'])/, '\\1')
|
|
146
167
|
end
|
|
168
|
+
result
|
|
147
169
|
end
|
|
148
170
|
|
|
149
171
|
def markdown(context, text)
|
|
@@ -166,7 +188,7 @@ module Datalog
|
|
|
166
188
|
end
|
|
167
189
|
|
|
168
190
|
def render(context)
|
|
169
|
-
attributes = References.attributes(@markup, context)
|
|
191
|
+
attributes = References.attributes(@markup, context, "figure")
|
|
170
192
|
id = References.validate_id(attributes["id"], "figure")
|
|
171
193
|
src = attributes["src"].to_s
|
|
172
194
|
raise Liquid::ArgumentError, "{% figure id=\"#{id}\" %} needs a src, the image it shows" if src.empty?
|
|
@@ -193,7 +215,7 @@ module Datalog
|
|
|
193
215
|
|
|
194
216
|
# The body holds the caption and then a Markdown table.
|
|
195
217
|
def render(context)
|
|
196
|
-
id = References.validate_id(References.attributes(@markup, context)["id"], "table")
|
|
218
|
+
id = References.validate_id(References.attributes(@markup, context, "table")["id"], "table")
|
|
197
219
|
html = References.markdown(context, super)
|
|
198
220
|
tables = html.scan(/<table\b/).size
|
|
199
221
|
unless tables == 1
|
data/_plugins/reproducibility.rb
CHANGED
|
@@ -96,9 +96,22 @@ module Datalog
|
|
|
96
96
|
def host_url(url, kind, *parts)
|
|
97
97
|
return unless url
|
|
98
98
|
|
|
99
|
-
|
|
99
|
+
uri = URI.parse(url)
|
|
100
|
+
host = uri.host.to_s.downcase.sub(/\Awww\./, "")
|
|
100
101
|
pattern = HOSTS.dig(host, kind)
|
|
101
|
-
|
|
102
|
+
return unless pattern
|
|
103
|
+
|
|
104
|
+
root = if host == "github.com"
|
|
105
|
+
uri.path.split("/").reject(&:empty?).first(2).join("/")
|
|
106
|
+
else
|
|
107
|
+
uri.path.sub(%r{/-/.*}, "").delete_prefix("/")
|
|
108
|
+
end
|
|
109
|
+
root = root.chomp("/").delete_suffix(".git")
|
|
110
|
+
# A ref is one route argument; a file retains its directory separators.
|
|
111
|
+
encoded = parts.each_with_index.map do |part, index|
|
|
112
|
+
index.zero? ? URI.encode_www_form_component(part).gsub("+", "%20") : encode_path(part)
|
|
113
|
+
end
|
|
114
|
+
"#{uri.scheme}://#{host}/#{root}#{format(pattern, *encoded)}"
|
|
102
115
|
rescue URI::InvalidURIError
|
|
103
116
|
nil
|
|
104
117
|
end
|
|
@@ -125,7 +138,11 @@ module Datalog
|
|
|
125
138
|
|
|
126
139
|
def doi_url(doi)
|
|
127
140
|
bare = bare_doi(doi)
|
|
128
|
-
"https://doi.org/#{bare}" if bare
|
|
141
|
+
"https://doi.org/#{encode_path(bare)}" if bare
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def encode_path(value)
|
|
145
|
+
value.to_s.split("/", -1).map { |part| URI.encode_www_form_component(part).gsub("+", "%20") }.join("/")
|
|
129
146
|
end
|
|
130
147
|
|
|
131
148
|
# What a link reads: the address without its scheme, or the site path.
|