datalog-theme 0.9.0 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +21 -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 +16 -14
- data/_plugins/licenses.rb +15 -3
- data/_plugins/math_preprocessor.rb +23 -1
- 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
|
|
@@ -440,16 +442,16 @@ module Jekyll
|
|
|
440
442
|
end
|
|
441
443
|
|
|
442
444
|
def normalize_src(src, site)
|
|
443
|
-
|
|
444
|
-
|
|
445
|
+
# Relative image URLs belong to the page, not to the source root. Leave
|
|
446
|
+
# them alone rather than offer another image's dimensions or variants.
|
|
447
|
+
return unless src&.start_with?("/") && !src.start_with?("//")
|
|
445
448
|
|
|
446
449
|
baseurl = site&.baseurl.to_s
|
|
447
|
-
normalized = src.
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
normalized
|
|
452
|
-
"/#{normalized}"
|
|
450
|
+
normalized = URI::DEFAULT_PARSER.unescape(src.split(/[?#]/, 2).first)
|
|
451
|
+
if !baseurl.empty? && (normalized == baseurl || normalized.start_with?("#{baseurl.chomp('/')}/"))
|
|
452
|
+
normalized = normalized.delete_prefix(baseurl.chomp("/"))
|
|
453
|
+
end
|
|
454
|
+
normalized
|
|
453
455
|
end
|
|
454
456
|
|
|
455
457
|
def image_config(site)
|
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
|
|
@@ -71,6 +71,7 @@ module MathPreprocessor
|
|
|
71
71
|
processed = CODE_PATTERNS.reduce(@content.dup) do |text, pattern|
|
|
72
72
|
text.gsub(pattern) { |match| mask(match) }
|
|
73
73
|
end
|
|
74
|
+
processed = normalize_inline_dollars(processed)
|
|
74
75
|
processed = apply_patterns(processed, DISPLAY_PATTERNS, display: true)
|
|
75
76
|
processed = apply_patterns(processed, INLINE_PATTERNS, display: false)
|
|
76
77
|
# A segment set aside can contain the placeholder of an earlier one.
|
|
@@ -85,6 +86,24 @@ module MathPreprocessor
|
|
|
85
86
|
"\x00#{@segments.size - 1}\x00"
|
|
86
87
|
end
|
|
87
88
|
|
|
89
|
+
# Kramdown accepts $$...$$ inside prose as inline math. A div there is
|
|
90
|
+
# escaped by Markdown and leaks its attributes into the visible article.
|
|
91
|
+
# Normalize before wrapping; code is already masked and standalone or
|
|
92
|
+
# multiline display equations retain their original delimiters.
|
|
93
|
+
def normalize_inline_dollars(text)
|
|
94
|
+
text.gsub(DISPLAY_PATTERNS.first[:regex]) do |expression|
|
|
95
|
+
match = Regexp.last_match
|
|
96
|
+
body = match[:body]
|
|
97
|
+
next expression if body.include?("\n") || body.strip.empty?
|
|
98
|
+
|
|
99
|
+
before = match.pre_match.split("\n", -1).last.to_s
|
|
100
|
+
after = match.post_match.split("\n", 2).first.to_s
|
|
101
|
+
next expression unless before.match?(/\S/) || after.match?(/\S/)
|
|
102
|
+
|
|
103
|
+
"$#{body.strip}$"
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
88
107
|
def apply_patterns(text, patterns, display: false)
|
|
89
108
|
patterns.reduce(text) do |result, pattern|
|
|
90
109
|
result.gsub(pattern[:regex]) do |match|
|
|
@@ -118,6 +137,9 @@ module MathPreprocessor
|
|
|
118
137
|
"aria-label" => alt_text,
|
|
119
138
|
"tabindex" => "0"
|
|
120
139
|
}
|
|
140
|
+
# Kramdown must not interpret TeX's escaped delimiters or underscores as
|
|
141
|
+
# Markdown inside an inline HTML span.
|
|
142
|
+
attributes["markdown"] = "0" unless display
|
|
121
143
|
|
|
122
144
|
attribute_string = attributes.map do |key, value|
|
|
123
145
|
next if value.nil? || value.strip.empty?
|
|
@@ -125,7 +147,7 @@ module MathPreprocessor
|
|
|
125
147
|
%(#{key}="#{CGI.escapeHTML(value)}")
|
|
126
148
|
end.compact.join(" ")
|
|
127
149
|
|
|
128
|
-
inner = "#{open}#{latex}#{close}"
|
|
150
|
+
inner = CGI.escapeHTML("#{open}#{latex}#{close}")
|
|
129
151
|
"<#{tag} #{attribute_string}>#{inner}</#{tag}>"
|
|
130
152
|
end
|
|
131
153
|
|
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.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "nokogiri"
|
|
4
|
+
|
|
5
|
+
module Datalog
|
|
6
|
+
# A table keeps its native semantics and desktop width while its enclosing
|
|
7
|
+
# region can scroll with either touch or the keyboard on a narrow screen.
|
|
8
|
+
module ResponsiveContent
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
def process(document)
|
|
12
|
+
return unless document.output_ext == ".html" && document.content.to_s.include?("<table")
|
|
13
|
+
|
|
14
|
+
html = Nokogiri::HTML5.fragment(document.content)
|
|
15
|
+
tables = html.css("table").to_a
|
|
16
|
+
tables.reject! do |table|
|
|
17
|
+
table["class"].to_s.split.include?("rouge-table") || table.ancestors(".content-table").any?
|
|
18
|
+
end
|
|
19
|
+
return if tables.empty?
|
|
20
|
+
|
|
21
|
+
locale = I18n.locale_code(document.site, document.data["lang"])
|
|
22
|
+
label = I18n.lookup(document.site, locale, "references.table") || "Table"
|
|
23
|
+
tables.each_with_index do |table, index|
|
|
24
|
+
wrapper = Nokogiri::XML::Node.new("div", html)
|
|
25
|
+
wrapper["class"] = "content-table"
|
|
26
|
+
wrapper["role"] = "region"
|
|
27
|
+
wrapper["tabindex"] = "0"
|
|
28
|
+
caption = table.at_css("caption")&.text.to_s.strip
|
|
29
|
+
wrapper["aria-label"] = caption.empty? ? "#{label} #{index + 1}" : caption
|
|
30
|
+
table.add_previous_sibling(wrapper)
|
|
31
|
+
wrapper.add_child(table)
|
|
32
|
+
end
|
|
33
|
+
document.content = html.to_html
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
Jekyll::Hooks.register %i[pages documents], :post_convert do |document|
|
|
39
|
+
Datalog::ResponsiveContent.process(document)
|
|
40
|
+
end
|
data/_plugins/revisions.rb
CHANGED
data/_plugins/scholarly.rb
CHANGED
|
@@ -40,6 +40,11 @@ module Datalog
|
|
|
40
40
|
end
|
|
41
41
|
|
|
42
42
|
module ScholarlyFilters
|
|
43
|
+
# JSON embedded in a script must not contain an HTML closing tag.
|
|
44
|
+
def jsonify_script(value)
|
|
45
|
+
jsonify(value).gsub("<", '\\u003c').gsub("\u2028", '\\u2028').gsub("\u2029", '\\u2029')
|
|
46
|
+
end
|
|
47
|
+
|
|
43
48
|
# A Liquid filter's name cannot end with "?".
|
|
44
49
|
def scholarly(page) # rubocop:disable Naming/PredicateMethod
|
|
45
50
|
Scholarly.scholarly?(page, @context["site"])
|
data/_plugins/statements.rb
CHANGED
|
@@ -28,7 +28,7 @@ module Datalog
|
|
|
28
28
|
end
|
|
29
29
|
|
|
30
30
|
def render(context)
|
|
31
|
-
attributes = References.attributes(@markup, context)
|
|
31
|
+
attributes = References.attributes(@markup, context, @kind)
|
|
32
32
|
id = References.validate_id(attributes["id"], @kind)
|
|
33
33
|
custom = attributes["label"]
|
|
34
34
|
References.validate_label(custom, @kind)
|
|
@@ -52,7 +52,7 @@ module Datalog
|
|
|
52
52
|
end
|
|
53
53
|
|
|
54
54
|
def render(context)
|
|
55
|
-
attributes = References.attributes(@markup, context)
|
|
55
|
+
attributes = References.attributes(@markup, context, "proof")
|
|
56
56
|
target = attributes["for"]
|
|
57
57
|
body = References.markdown(context, super)
|
|
58
58
|
heading_id = "proof-#{target ? References.validate_id(target, 'proof') : Statements.next_proof(context)}"
|