datalog-theme 0.6.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 +7 -0
- data/CHANGELOG.md +162 -0
- data/CITATION.cff +29 -0
- data/LICENSE +21 -0
- data/README.md +257 -0
- data/_data/academic.yml +217 -0
- data/_data/cdn-integrity.yml +51 -0
- data/_data/config/author.yml +121 -0
- data/_data/config/features.yml +262 -0
- data/_data/config/site.yml +42 -0
- data/_data/config/theme.yml +181 -0
- data/_data/datasets.yml +28 -0
- data/_data/i18n/en.yml +438 -0
- data/_data/i18n/es.yml +438 -0
- data/_data/i18n/pt.yml +438 -0
- data/_data/js_manifest.json +22 -0
- data/_data/js_meta.json +371 -0
- data/_data/navigation.yml +145 -0
- data/_data/projects.yml +41 -0
- data/_data/publications.yml +28 -0
- data/_data/social.yml +73 -0
- data/_data/visualizations.yml +51 -0
- data/_includes/analytics/dashboard.html +149 -0
- data/_includes/analytics.html +9 -0
- data/_includes/components/academic-dashboard.html +347 -0
- data/_includes/components/advanced-search.html +682 -0
- data/_includes/components/api-function.html +94 -0
- data/_includes/components/author-bio.html +166 -0
- data/_includes/components/bookmark-system.html +96 -0
- data/_includes/components/breadcrumbs.html +89 -0
- data/_includes/components/citation-tools.html +94 -0
- data/_includes/components/comments.html +244 -0
- data/_includes/components/content-provenance.html +43 -0
- data/_includes/components/content-recommendations.html +228 -0
- data/_includes/components/difficulty-badge.html +84 -0
- data/_includes/components/email-preferences.html +200 -0
- data/_includes/components/enhanced-code-block.html +212 -0
- data/_includes/components/enhanced-metadata.html +228 -0
- data/_includes/components/enhanced-toc.html +209 -0
- data/_includes/components/hero.html +15 -0
- data/_includes/components/language-switcher.html +396 -0
- data/_includes/components/math-fallback.html +19 -0
- data/_includes/components/navigation-enhancements.html +454 -0
- data/_includes/components/newsletter-signup.html +178 -0
- data/_includes/components/open-science-badges.html +16 -0
- data/_includes/components/package-install.html +194 -0
- data/_includes/components/performance-monitor.html +170 -0
- data/_includes/components/popular-posts.html +233 -0
- data/_includes/components/post-hero.html +62 -0
- data/_includes/components/reading-progress.html +133 -0
- data/_includes/components/reading-time.html +121 -0
- data/_includes/components/responsive-image.html +63 -0
- data/_includes/components/search-facets.html +307 -0
- data/_includes/components/series-navigation.html +124 -0
- data/_includes/components/social-proof.html +34 -0
- data/_includes/components/social-share.html +119 -0
- data/_includes/components/user-preferences.html +566 -0
- data/_includes/components/visualization-card.html +24 -0
- data/_includes/components/viz-table-fallback.html +19 -0
- data/_includes/critical-css/default.html +0 -0
- data/_includes/critical-css/home.html +0 -0
- data/_includes/critical-css/post.html +0 -0
- data/_includes/csp-meta.html +17 -0
- data/_includes/footer/nav-column.html +31 -0
- data/_includes/footer.html +76 -0
- data/_includes/head.html +337 -0
- data/_includes/header/navigation.html +48 -0
- data/_includes/header.html +70 -0
- data/_includes/helpers/array-to-sentence.html +21 -0
- data/_includes/helpers/date-format.html +16 -0
- data/_includes/helpers/link-with-icon.html +26 -0
- data/_includes/layouts/default/article.html +67 -0
- data/_includes/meta/language-attributes.html +25 -0
- data/_includes/meta/math-config.html +21 -0
- data/_includes/meta/schema.html +153 -0
- data/_includes/meta/scripts-loader.html +52 -0
- data/_includes/post/related-posts.html +63 -0
- data/_includes/scripts.html +72 -0
- data/_includes/skip-link.html +1 -0
- data/_includes/toc.html +2 -0
- data/_layouts/archive.html +282 -0
- data/_layouts/dataset.html +52 -0
- data/_layouts/default.html +33 -0
- data/_layouts/home.html +48 -0
- data/_layouts/notebook.html +173 -0
- data/_layouts/package.html +238 -0
- data/_layouts/page.html +16 -0
- data/_layouts/portfolio.html +41 -0
- data/_layouts/post-sidebar.html +183 -0
- data/_layouts/post.html +338 -0
- data/_layouts/project.html +252 -0
- data/_layouts/research.html +504 -0
- data/_plugins/analytics_dashboard.rb +315 -0
- data/_plugins/config_validator.rb +397 -0
- data/_plugins/csp_generator.rb +115 -0
- data/_plugins/datalog_bibliography.rb +19 -0
- data/_plugins/datalog_comments.rb +18 -0
- data/_plugins/datalog_slides.rb +20 -0
- data/_plugins/front_matter_compat.rb +109 -0
- data/_plugins/i18n.rb +160 -0
- data/_plugins/image_optimizer.rb +445 -0
- data/_plugins/math_preprocessor.rb +185 -0
- data/_plugins/notebook_converter.rb +1107 -0
- data/_plugins/plugin_loader.rb +185 -0
- data/_plugins/publications_generator.rb +234 -0
- data/_plugins/reading_time.rb +15 -0
- data/_plugins/search_normalizer.rb +74 -0
- data/_plugins/sri_filter.rb +18 -0
- data/_plugins/toc_filter.rb +68 -0
- data/_plugins/warning_filter.rb +3 -0
- data/_sass/_base.scss +142 -0
- data/_sass/_components.scss +2380 -0
- data/_sass/_header.scss +470 -0
- data/_sass/_interactive.scss +344 -0
- data/_sass/_layout.scss +1760 -0
- data/_sass/_mathematical.scss +368 -0
- data/_sass/_mixins.scss +99 -0
- data/_sass/_package-docs.scss +715 -0
- data/_sass/_phase1-enhancements.scss +721 -0
- data/_sass/_phase3-enhancements.scss +874 -0
- data/_sass/_phase4-enhancements.scss +1214 -0
- data/_sass/_phase5-enhancements.scss +414 -0
- data/_sass/_search.scss +654 -0
- data/_sass/_syntax-highlighting.scss +128 -0
- data/_sass/_theme.scss +79 -0
- data/_sass/_typography.scss +148 -0
- data/_sass/_utilities.scss +334 -0
- data/_sass/_variables.scss +153 -0
- data/_sass/_visualizations.scss +242 -0
- data/assets/css/main.scss +4 -0
- data/assets/img/20220607123041_detail.001.png +0 -0
- data/assets/img/favicons/android-chrome-192x192.png +0 -0
- data/assets/img/favicons/android-chrome-512x512.png +0 -0
- data/assets/img/favicons/apple-touch-icon.png +0 -0
- data/assets/img/favicons/favicon-16x16.png +0 -0
- data/assets/img/favicons/favicon-32x32.png +0 -0
- data/assets/img/favicons/favicon.ico +0 -0
- data/assets/img/favicons/site.webmanifest +21 -0
- data/assets/img/portfolio-placeholder.svg +20 -0
- data/assets/js/academic.js +262 -0
- data/assets/js/analytics-dashboard.js +382 -0
- data/assets/js/core/dark-mode.js +79 -0
- data/assets/js/core/github-cards.js +123 -0
- data/assets/js/core/language-filter.js +69 -0
- data/assets/js/core/navigation.js +184 -0
- data/assets/js/core/scroll-progress.js +45 -0
- data/assets/js/core/search-hotkeys.js +62 -0
- data/assets/js/core/skip-links.js +62 -0
- data/assets/js/loader.js +163 -0
- data/assets/js/main.js +23 -0
- data/assets/js/math.js +818 -0
- data/assets/js/notebook.js +158 -0
- data/assets/js/search/analytics.js +91 -0
- data/assets/js/search/app.js +271 -0
- data/assets/js/search/autocomplete.js +120 -0
- data/assets/js/search/engine.js +260 -0
- data/assets/js/search/filters.js +38 -0
- data/assets/js/search/render.js +217 -0
- data/assets/js/search/utils.js +99 -0
- data/assets/js/search.js +354 -0
- data/assets/js/visualizations.js +816 -0
- data/assets/publications/datalog-publications.bib +8 -0
- data/assets/publications/datalog-publications.ris +9 -0
- data/assets/publications/publications.bib +30 -0
- data/assets/templates/diogo-ribeiro-cv.md +31 -0
- data/assets/templates/diogo-ribeiro-cv.tex +32 -0
- data/bin/datalog +26 -0
- data/datalog-theme.gemspec +72 -0
- data/lib/datalog/cli.rb +563 -0
- data/lib/datalog/plugin_system/dependency_resolver.rb +223 -0
- data/lib/datalog/plugin_system.rb +237 -0
- data/lib/datalog/plugins/citations.rb +182 -0
- data/lib/datalog/plugins/comments.rb +183 -0
- data/lib/datalog/plugins/search.rb +45 -0
- data/lib/datalog/plugins/slides.rb +121 -0
- data/lib/datalog/theme/theme.rb +18 -0
- data/lib/datalog/theme/version.rb +7 -0
- data/lib/datalog/warning_filter.rb +28 -0
- data/lib/datalog-theme.rb +17 -0
- metadata +555 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "jekyll/utils"
|
|
4
|
+
|
|
5
|
+
require_relative "../lib/datalog/plugin_system"
|
|
6
|
+
|
|
7
|
+
Dir[File.expand_path("../lib/datalog/plugins/**/*.rb", __dir__)].each do |plugin_file|
|
|
8
|
+
require plugin_file
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
module Datalog
|
|
12
|
+
class PluginLoader < Jekyll::Generator
|
|
13
|
+
safe true
|
|
14
|
+
priority :highest
|
|
15
|
+
|
|
16
|
+
def generate(site)
|
|
17
|
+
plugin_classes = enabled_plugins(site).filter_map do |identifier|
|
|
18
|
+
resolve_plugin_class(identifier)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
resolver = Datalog::PluginSystem::DependencyResolver.new(
|
|
22
|
+
plugin_classes,
|
|
23
|
+
available: Datalog::PluginSystem.available_plugins
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
ordered_classes = resolver.resolve
|
|
27
|
+
log_dependency_resolution(resolver, ordered_classes)
|
|
28
|
+
|
|
29
|
+
instances = ordered_classes.filter_map do |klass|
|
|
30
|
+
instantiate_plugin(site, klass)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
Datalog::PluginSystem.activate!(site, instances)
|
|
34
|
+
register_liquid_tags(instances)
|
|
35
|
+
|
|
36
|
+
site.config["datalog_plugin_state"] = {
|
|
37
|
+
"active" => instances.map(&:id),
|
|
38
|
+
"available" => Datalog::PluginSystem.available_plugins.keys.sort
|
|
39
|
+
}
|
|
40
|
+
rescue Datalog::PluginSystem::PluginDependencyError => e
|
|
41
|
+
logger.error("datalog plugin", e.message)
|
|
42
|
+
raise e
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def enabled_plugins(site)
|
|
48
|
+
config = site.config.fetch("datalog_plugins", {})
|
|
49
|
+
|
|
50
|
+
plugins =
|
|
51
|
+
case config
|
|
52
|
+
when Array
|
|
53
|
+
config
|
|
54
|
+
when Hash
|
|
55
|
+
Array(config["enabled"] || config["plugins"] || [])
|
|
56
|
+
else
|
|
57
|
+
[]
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
plugins.each_with_object([]) do |identifier, acc|
|
|
61
|
+
next if identifier.nil?
|
|
62
|
+
|
|
63
|
+
value = identifier.to_s.strip
|
|
64
|
+
next if value.empty?
|
|
65
|
+
|
|
66
|
+
acc << value unless acc.include?(value)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def instantiate_plugin(site, klass)
|
|
71
|
+
identifier = klass.id
|
|
72
|
+
config = fetch_plugin_config(site, identifier)
|
|
73
|
+
klass.new(site, config)
|
|
74
|
+
rescue StandardError => e
|
|
75
|
+
logger.error("datalog plugin", "#{identifier} failed to initialize: #{e.message}")
|
|
76
|
+
nil
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def resolve_plugin_class(identifier)
|
|
80
|
+
klass = Datalog::PluginSystem.available_plugins[identifier]
|
|
81
|
+
unless klass
|
|
82
|
+
logger.warn("datalog plugin", "#{identifier} is not registered; skipping")
|
|
83
|
+
return
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
klass
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def fetch_plugin_config(site, identifier)
|
|
90
|
+
config = site.config.fetch("datalog_plugins", {})
|
|
91
|
+
return {} unless config.is_a?(Hash)
|
|
92
|
+
|
|
93
|
+
options = config.fetch("options", {})
|
|
94
|
+
return {} unless options.is_a?(Hash)
|
|
95
|
+
|
|
96
|
+
key = identifier
|
|
97
|
+
value = options[key] || options[key.to_s] || options[key.to_sym]
|
|
98
|
+
value.is_a?(Hash) ? value : {}
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def register_liquid_tags(instances)
|
|
102
|
+
instances.each do |plugin|
|
|
103
|
+
next unless plugin.respond_to?(:custom_liquid_tags)
|
|
104
|
+
|
|
105
|
+
tags = plugin.custom_liquid_tags
|
|
106
|
+
unless tags.is_a?(Hash)
|
|
107
|
+
logger.warn("datalog plugin", "#{plugin.id} returned invalid Liquid tags payload; expected a hash")
|
|
108
|
+
next
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
tags.each do |tag_name, tag_class|
|
|
112
|
+
next if tag_name.to_s.empty? || tag_class.nil?
|
|
113
|
+
|
|
114
|
+
Liquid::Template.register_tag(tag_name, tag_class)
|
|
115
|
+
rescue StandardError => e
|
|
116
|
+
logger.warn("datalog plugin", "#{plugin.id} failed to register #{tag_name} tag: #{e.message}")
|
|
117
|
+
end
|
|
118
|
+
rescue StandardError => e
|
|
119
|
+
logger.warn("datalog plugin", "#{plugin.id} failed while registering Liquid tags: #{e.message}")
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def logger
|
|
124
|
+
Jekyll.logger
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def log_dependency_resolution(resolver, ordered_classes)
|
|
128
|
+
return unless debug_logging?
|
|
129
|
+
|
|
130
|
+
identifiers = ordered_classes.map(&:id)
|
|
131
|
+
logger.debug("datalog plugin", "resolved load order: #{identifiers.join(' -> ')}")
|
|
132
|
+
|
|
133
|
+
graph = resolver.dependency_graph
|
|
134
|
+
return if graph.empty?
|
|
135
|
+
|
|
136
|
+
graph_summary = graph.map do |plugin_id, dependencies|
|
|
137
|
+
deps = Array(dependencies).map(&:to_s).sort
|
|
138
|
+
deps.empty? ? "#{plugin_id}: []" : "#{plugin_id}: [#{deps.join(', ')}]"
|
|
139
|
+
end.join("; ")
|
|
140
|
+
|
|
141
|
+
logger.debug("datalog plugin", "dependency graph: #{graph_summary}")
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def debug_logging?
|
|
145
|
+
logger.respond_to?(:debug?) && logger.debug?
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
module Datalog
|
|
151
|
+
module PluginLoaderHooks
|
|
152
|
+
HOOK_SCOPES = %i[pages documents posts].freeze
|
|
153
|
+
|
|
154
|
+
module_function
|
|
155
|
+
|
|
156
|
+
def register!
|
|
157
|
+
HOOK_SCOPES.each do |scope|
|
|
158
|
+
Jekyll::Hooks.register scope, :pre_render do |document, payload|
|
|
159
|
+
Datalog::PluginSystem.run_hook(:before_render, document, payload)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
Jekyll::Hooks.register scope, :post_render do |document|
|
|
163
|
+
extensions = Datalog::PluginSystem.collect_hook(:search_indexing, document)
|
|
164
|
+
next if extensions.nil? || extensions.empty?
|
|
165
|
+
|
|
166
|
+
existing = document.data["datalog_search_extensions"] || {}
|
|
167
|
+
document.data["datalog_search_extensions"] = deep_merge_extensions(existing, extensions)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
Jekyll::Hooks.register scope, :post_write do |document|
|
|
171
|
+
Datalog::PluginSystem.run_hook(:after_post_process, document)
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def deep_merge_extensions(existing, extensions)
|
|
177
|
+
return extensions unless existing.is_a?(Hash)
|
|
178
|
+
return existing unless extensions.is_a?(Hash)
|
|
179
|
+
|
|
180
|
+
Jekyll::Utils.deep_merge_hashes(existing, extensions)
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
Datalog::PluginLoaderHooks.register!
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jekyll
|
|
4
|
+
class PublicationsGenerator < Generator
|
|
5
|
+
safe true
|
|
6
|
+
priority :low
|
|
7
|
+
|
|
8
|
+
def generate(site)
|
|
9
|
+
publications_data = site.data["publications"] ||= {}
|
|
10
|
+
settings = publications_data["settings"] || {}
|
|
11
|
+
config_source = site.config.dig("theme_options", "publications", "bibtex_source")
|
|
12
|
+
bibtex_source = settings["bibtex_source"] || config_source
|
|
13
|
+
|
|
14
|
+
imported_entries = []
|
|
15
|
+
if bibtex_source
|
|
16
|
+
bibtex_path = site.in_source_dir(bibtex_source)
|
|
17
|
+
if File.exist?(bibtex_path)
|
|
18
|
+
content = File.read(bibtex_path, encoding: "UTF-8")
|
|
19
|
+
imported_entries = parse_bibtex_entries(content)
|
|
20
|
+
else
|
|
21
|
+
Jekyll.logger.warn("PublicationsGenerator:", "BibTeX source not found at #{bibtex_path}")
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
manual_entries = publications_data["manual_entries"] || []
|
|
26
|
+
combined = (imported_entries + manual_entries).map { |entry| normalize_entry(entry) }
|
|
27
|
+
|
|
28
|
+
academic_citations = site.data.dig("academic", "citations", "per_publication") || {}
|
|
29
|
+
citations_map = {}
|
|
30
|
+
combined.each do |entry|
|
|
31
|
+
identifier = entry["id"] || parameterize(entry["title"])
|
|
32
|
+
citation_info = locate_citation_info(academic_citations, identifier)
|
|
33
|
+
if citation_info
|
|
34
|
+
entry["citations"] = citation_info["total"] || citation_info[:total]
|
|
35
|
+
entry["citations_last_updated"] = citation_info["last_updated"] || citation_info[:last_updated]
|
|
36
|
+
end
|
|
37
|
+
next unless identifier
|
|
38
|
+
|
|
39
|
+
citations_map[identifier] = {
|
|
40
|
+
"total" => entry["citations"]&.to_i,
|
|
41
|
+
"last_updated" => entry["citations_last_updated"]
|
|
42
|
+
}
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
sort_order = (settings["sort"] || settings["sort_order"] || "descending").to_s
|
|
46
|
+
combined.sort_by! { |entry| [entry["year"] ? entry["year"].to_i : 0, entry["title"].to_s.downcase] }
|
|
47
|
+
combined.reverse! if sort_order == "descending"
|
|
48
|
+
|
|
49
|
+
group_field = settings["group_by"] || site.config.dig("theme_options", "publications", "group_by")
|
|
50
|
+
grouped = group_entries(combined, group_field, sort_order)
|
|
51
|
+
|
|
52
|
+
include_bibliography = if settings.key?("include_bibliography")
|
|
53
|
+
settings["include_bibliography"]
|
|
54
|
+
else
|
|
55
|
+
site.config.dig(
|
|
56
|
+
"theme_options", "publications", "include_bibliography"
|
|
57
|
+
)
|
|
58
|
+
end
|
|
59
|
+
exports = settings["export_formats"] || site.config.dig("theme_options", "publications", "export_formats") || []
|
|
60
|
+
|
|
61
|
+
metrics_override = site.data.dig("academic", "citations", "metrics") || {}
|
|
62
|
+
metrics = compute_metrics(combined, citations_map, metrics_override)
|
|
63
|
+
|
|
64
|
+
publications_data["imported"] = imported_entries
|
|
65
|
+
publications_data["entries"] = combined
|
|
66
|
+
publications_data["grouped"] = grouped if grouped
|
|
67
|
+
publications_data["citation_totals"] = citations_map
|
|
68
|
+
publications_data["metrics"] = metrics
|
|
69
|
+
publications_data["yearly_totals"] = metrics["yearly_totals"]
|
|
70
|
+
return unless include_bibliography
|
|
71
|
+
|
|
72
|
+
publications_data["bibliography"] = build_bibliography(combined)
|
|
73
|
+
publications_data["export_formats"] = exports
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
ENTRY_REGEX = /@(?<type>\w+)\s*\{\s*(?<key>[^,]+),\s*(?<fields>.*?)\}\s*(?=@|\z)/m
|
|
79
|
+
|
|
80
|
+
def parse_bibtex_entries(content)
|
|
81
|
+
entries = []
|
|
82
|
+
content.to_s.scan(ENTRY_REGEX) do |type, key, raw_fields|
|
|
83
|
+
fields = {}
|
|
84
|
+
raw_fields.lines.each do |line|
|
|
85
|
+
cleaned = line.strip
|
|
86
|
+
next if cleaned.empty?
|
|
87
|
+
|
|
88
|
+
cleaned = cleaned.delete_suffix(",")
|
|
89
|
+
name, value = cleaned.split("=", 2)
|
|
90
|
+
next unless name && value
|
|
91
|
+
|
|
92
|
+
name = name.strip.downcase
|
|
93
|
+
value = value.strip
|
|
94
|
+
value = value.gsub(/\A["{]+/, "").gsub(/["}]+\z/, "")
|
|
95
|
+
fields[name] = value.strip
|
|
96
|
+
end
|
|
97
|
+
entries << {
|
|
98
|
+
"id" => key.strip,
|
|
99
|
+
"type" => type.strip.downcase,
|
|
100
|
+
"title" => fields["title"],
|
|
101
|
+
"authors" => parse_authors(fields["author"]),
|
|
102
|
+
"venue" => fields["journal"] || fields["booktitle"] || fields["institution"],
|
|
103
|
+
"year" => fields["year"],
|
|
104
|
+
"doi" => fields["doi"],
|
|
105
|
+
"url" => fields["url"],
|
|
106
|
+
"raw" => fields
|
|
107
|
+
}
|
|
108
|
+
end
|
|
109
|
+
entries
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def parse_authors(raw)
|
|
113
|
+
return [] unless raw
|
|
114
|
+
|
|
115
|
+
raw.split(/\s+and\s+/i).map(&:strip)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def normalize_entry(entry)
|
|
119
|
+
normalized = entry.transform_keys(&:to_s)
|
|
120
|
+
normalized["authors"] = parse_authors(normalized["authors"]) if normalized["authors"].is_a?(String)
|
|
121
|
+
normalized["year"] = normalized["year"].to_s if normalized["year"]
|
|
122
|
+
normalized
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def locate_citation_info(map, identifier)
|
|
126
|
+
return nil unless identifier
|
|
127
|
+
|
|
128
|
+
map[identifier] || map[identifier.to_s] || map[identifier.to_s.downcase]
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def parameterize(value)
|
|
132
|
+
return nil unless value
|
|
133
|
+
|
|
134
|
+
value.to_s.downcase.strip.gsub(/[^a-z0-9]+/, "-").gsub(/^-|-$/, "")
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def group_entries(entries, field, sort_order)
|
|
138
|
+
return nil unless field && !field.to_s.empty?
|
|
139
|
+
|
|
140
|
+
grouped = entries.group_by { |entry| entry[field.to_s] || "Other" }
|
|
141
|
+
sorted = grouped.sort_by do |group_key, _|
|
|
142
|
+
group_value = group_key.to_s
|
|
143
|
+
if group_value.match?(/\A\d+\z/)
|
|
144
|
+
group_value.to_i
|
|
145
|
+
else
|
|
146
|
+
group_value.downcase
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
sorted.reverse! if sort_order == "descending"
|
|
150
|
+
sorted
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def compute_metrics(entries, citations_map, override)
|
|
154
|
+
yearly_totals = Hash.new(0)
|
|
155
|
+
citation_counts = []
|
|
156
|
+
entries.each do |entry|
|
|
157
|
+
year = entry["year"]
|
|
158
|
+
citations = entry["citations"]
|
|
159
|
+
citation_value = citations.to_i if citations
|
|
160
|
+
if citation_value&.positive?
|
|
161
|
+
citation_counts << citation_value
|
|
162
|
+
yearly_totals[year.to_s] += citation_value if year
|
|
163
|
+
elsif year
|
|
164
|
+
yearly_totals[year.to_s] += 1
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
citation_counts.sort! { |a, b| b <=> a }
|
|
169
|
+
total = citation_counts.reduce(0, :+)
|
|
170
|
+
h_index = 0
|
|
171
|
+
citation_counts.each_with_index do |count, index|
|
|
172
|
+
h_index = index + 1 if count >= index + 1
|
|
173
|
+
end
|
|
174
|
+
i10_index = citation_counts.count { |count| count >= 10 }
|
|
175
|
+
|
|
176
|
+
metrics = {
|
|
177
|
+
"total" => total,
|
|
178
|
+
"h_index" => h_index,
|
|
179
|
+
"i10_index" => i10_index,
|
|
180
|
+
"per_publication" => citations_map,
|
|
181
|
+
"yearly_totals" => yearly_totals
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
override.each do |key, value|
|
|
185
|
+
metrics[key.to_s] = if value.is_a?(Hash)
|
|
186
|
+
merge_nested_hash(metrics[key.to_s], value)
|
|
187
|
+
else
|
|
188
|
+
value
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
metrics
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def merge_nested_hash(base, override)
|
|
196
|
+
base = (base || {}).transform_keys(&:to_s)
|
|
197
|
+
override.each do |key, value|
|
|
198
|
+
key = key.to_s
|
|
199
|
+
base[key] = if value.is_a?(Hash)
|
|
200
|
+
merge_nested_hash(base[key], value)
|
|
201
|
+
else
|
|
202
|
+
value
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
base
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def build_bibliography(entries)
|
|
209
|
+
entries.map { |entry| format_bibtex(entry) }.compact
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def format_bibtex(entry)
|
|
213
|
+
type = entry["type"] || "article"
|
|
214
|
+
identifier = entry["id"] || parameterize(entry["title"]) || "reference"
|
|
215
|
+
authors = Array(entry["authors"]).map { |author| author.to_s.strip }.reject(&:empty?)
|
|
216
|
+
buffer = []
|
|
217
|
+
buffer << "@#{type}{ #{identifier},"
|
|
218
|
+
buffer << " title = { #{entry['title']} }," if entry["title"]
|
|
219
|
+
buffer << " author = { #{authors.join(' and ')} }," if authors.any?
|
|
220
|
+
buffer << " year = { #{entry['year']} }," if entry["year"]
|
|
221
|
+
buffer << " journal = { #{entry['venue']} }," if entry["venue"]
|
|
222
|
+
buffer << " doi = { #{entry['doi']} }," if entry["doi"]
|
|
223
|
+
buffer << " url = { #{entry['url']} }," if entry["url"]
|
|
224
|
+
raw_fields = entry["raw"] || {}
|
|
225
|
+
%w[volume number pages publisher].each do |field|
|
|
226
|
+
next unless raw_fields[field]
|
|
227
|
+
|
|
228
|
+
buffer << " #{field} = { #{raw_fields[field]} },"
|
|
229
|
+
end
|
|
230
|
+
buffer << "}"
|
|
231
|
+
buffer.join("\n")
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jekyll
|
|
4
|
+
module ReadingTimeFilter
|
|
5
|
+
WORDS_PER_MINUTE = 180.0
|
|
6
|
+
|
|
7
|
+
def reading_time(input)
|
|
8
|
+
words = input.to_s.split.size
|
|
9
|
+
minutes = (words / WORDS_PER_MINUTE).ceil
|
|
10
|
+
minutes.positive? ? minutes : 1
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
Liquid::Template.register_filter(Jekyll::ReadingTimeFilter)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
begin
|
|
4
|
+
require "unicode_normalize"
|
|
5
|
+
UNICODE_NORMALIZE_SUPPORTED = true
|
|
6
|
+
rescue LoadError
|
|
7
|
+
UNICODE_NORMALIZE_SUPPORTED = false
|
|
8
|
+
warn "[search_normalizer] unicode_normalize gem not available; falling back to basic normalization"
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
module Datalog
|
|
12
|
+
module SearchFilters
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
def normalize_search(input)
|
|
16
|
+
value = input.to_s
|
|
17
|
+
return "" if value.empty?
|
|
18
|
+
|
|
19
|
+
normalized = if UNICODE_NORMALIZE_SUPPORTED && value.respond_to?(:unicode_normalize)
|
|
20
|
+
value.unicode_normalize(:nfkd).gsub(/\p{Mn}/, "")
|
|
21
|
+
else
|
|
22
|
+
transliterate(value)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
normalized.downcase.strip
|
|
26
|
+
rescue StandardError
|
|
27
|
+
input.to_s.downcase
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def transliterate(value)
|
|
31
|
+
value.encode("ASCII", fallback: lambda { |char|
|
|
32
|
+
approximate_character(char)
|
|
33
|
+
}, invalid: :replace, undef: :replace, replace: "")
|
|
34
|
+
rescue Encoding::UndefinedConversionError, Encoding::InvalidByteSequenceError
|
|
35
|
+
value
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def approximate_character(char)
|
|
39
|
+
@transliteration_map ||= build_transliteration_map
|
|
40
|
+
@transliteration_map.fetch(char, "")
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def build_transliteration_map
|
|
44
|
+
basic_map = {}
|
|
45
|
+
|
|
46
|
+
accents = {
|
|
47
|
+
"ÀÁÂÃÄÅàáâãäå" => "a",
|
|
48
|
+
"ÈÉÊËèéêë" => "e",
|
|
49
|
+
"ÌÍÎÏìíîï" => "i",
|
|
50
|
+
"ÒÓÔÕÖØòóôõöø" => "o",
|
|
51
|
+
"ÙÚÛÜùúûü" => "u",
|
|
52
|
+
"Çç" => "c",
|
|
53
|
+
"Ññ" => "n",
|
|
54
|
+
"Ýýÿ" => "y",
|
|
55
|
+
"Ææ" => "ae",
|
|
56
|
+
"Œœ" => "oe"
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
accents.each do |chars, replacement|
|
|
60
|
+
chars.each_char { |char| basic_map[char] = replacement }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
basic_map
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def normalize_search_array(values)
|
|
67
|
+
Array(values)
|
|
68
|
+
.map { |value| normalize_search(value) }
|
|
69
|
+
.reject(&:empty?)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
Liquid::Template.register_filter(Datalog::SearchFilters)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jekyll
|
|
4
|
+
module SriFilter
|
|
5
|
+
def add_sri(url)
|
|
6
|
+
site = @context.registers[:site]
|
|
7
|
+
integrity_map = site.data["cdn-integrity"] || {}
|
|
8
|
+
entry = integrity_map[url]
|
|
9
|
+
return url unless entry
|
|
10
|
+
|
|
11
|
+
attributes = "#{url}\" integrity=\"#{entry['integrity']}"
|
|
12
|
+
attributes += "\" crossorigin=\"#{entry['crossorigin']}" if entry["crossorigin"]
|
|
13
|
+
attributes
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
Liquid::Template.register_filter(Jekyll::SriFilter)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "nokogiri"
|
|
4
|
+
|
|
5
|
+
# `tocify` builds a nested list of links to the headings of a rendered HTML
|
|
6
|
+
# fragment. The table-of-contents includes (`components/enhanced-toc.html`,
|
|
7
|
+
# `toc.html`) call it as `content | tocify: h_min, h_max`.
|
|
8
|
+
#
|
|
9
|
+
# Headings keep the ids kramdown gives them; a heading without an id gets a
|
|
10
|
+
# slug of its text, which still needs an anchor in the page to be useful.
|
|
11
|
+
module Jekyll
|
|
12
|
+
module TocifyFilter
|
|
13
|
+
def tocify(html, h_min = 2, h_max = 4)
|
|
14
|
+
return "" if html.nil? || html.to_s.strip.empty?
|
|
15
|
+
|
|
16
|
+
lowest = h_min.to_i.clamp(1, 6)
|
|
17
|
+
highest = h_max.to_i.clamp(lowest, 6)
|
|
18
|
+
fragment = Nokogiri::HTML::DocumentFragment.parse(html.to_s)
|
|
19
|
+
selector = (lowest..highest).map { |level| "h#{level}" }.join(",")
|
|
20
|
+
|
|
21
|
+
headings = fragment.css(selector).filter_map do |node|
|
|
22
|
+
text = node.text.strip
|
|
23
|
+
next if text.empty?
|
|
24
|
+
|
|
25
|
+
id = node["id"].to_s.strip
|
|
26
|
+
id = Jekyll::Utils.slugify(text) if id.empty?
|
|
27
|
+
[node.name[1].to_i, id, text]
|
|
28
|
+
end
|
|
29
|
+
return "" if headings.empty?
|
|
30
|
+
|
|
31
|
+
tocify_render_list(headings, lowest)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def tocify_render_list(headings, top_level)
|
|
37
|
+
out = +""
|
|
38
|
+
open_levels = []
|
|
39
|
+
headings.each do |level, id, text|
|
|
40
|
+
level = [level, top_level].max
|
|
41
|
+
if open_levels.empty?
|
|
42
|
+
out << %(<ul class="toc-list">)
|
|
43
|
+
open_levels << level
|
|
44
|
+
elsif level > open_levels.last
|
|
45
|
+
out << "<ul>"
|
|
46
|
+
open_levels << level
|
|
47
|
+
else
|
|
48
|
+
out << "</li>"
|
|
49
|
+
while open_levels.size > 1 && level < open_levels.last
|
|
50
|
+
out << "</ul></li>"
|
|
51
|
+
open_levels.pop
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
out << %(<li class="toc-item toc-level-#{level}"><a href="##{id}">#{tocify_escape_text(text)}</a>)
|
|
55
|
+
end
|
|
56
|
+
out << "</li>"
|
|
57
|
+
out << "</ul></li>" * (open_levels.size - 1)
|
|
58
|
+
out << "</ul>"
|
|
59
|
+
out
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def tocify_escape_text(text)
|
|
63
|
+
text.gsub("&", "&").gsub("<", "<").gsub(">", ">").gsub('"', """)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
Liquid::Template.register_filter(Jekyll::TocifyFilter)
|