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,223 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "set"
|
|
4
|
+
|
|
5
|
+
module Datalog
|
|
6
|
+
module PluginSystem
|
|
7
|
+
class DependencyResolver
|
|
8
|
+
attr_reader :plugins, :available_plugins
|
|
9
|
+
|
|
10
|
+
def initialize(plugins, options = {})
|
|
11
|
+
@plugins = Array(plugins).compact
|
|
12
|
+
@available_plugins = options.fetch(:available, {})
|
|
13
|
+
@graph = {}
|
|
14
|
+
@plugin_map = {}
|
|
15
|
+
@load_order = nil
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def resolve
|
|
19
|
+
build_graph
|
|
20
|
+
validate_dependencies!
|
|
21
|
+
order = perform_topological_sort
|
|
22
|
+
@load_order = order.map { |identifier| @plugin_map[identifier] }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def load_order
|
|
26
|
+
@load_order ||= resolve
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def dependency_graph
|
|
30
|
+
ensure_graph_built
|
|
31
|
+
@graph.transform_values { |meta| meta[:dependencies].dup }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def to_dot(order: load_order)
|
|
35
|
+
ensure_graph_built
|
|
36
|
+
ordered_ids = Array(order).map { |klass| klass.respond_to?(:id) ? klass.id : klass.to_s }
|
|
37
|
+
lines = []
|
|
38
|
+
lines << "digraph PluginDependencies {"
|
|
39
|
+
lines << " rankdir=LR;"
|
|
40
|
+
|
|
41
|
+
@graph.each_key do |identifier|
|
|
42
|
+
klass = @plugin_map[identifier]
|
|
43
|
+
priority_label = klass&.respond_to?(:priority) ? klass.priority.to_s : "normal" # rubocop:disable Style/SafeNavigation
|
|
44
|
+
lines << %( "#{identifier}" [label="#{identifier}\\npriority: #{priority_label}"];)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
@graph.each do |identifier, meta|
|
|
48
|
+
meta[:dependencies].each do |dep|
|
|
49
|
+
lines << %( "#{identifier}" -> "#{dep}";)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
if ordered_ids.any?
|
|
54
|
+
order_comment = ordered_ids.each_with_index.map { |id, idx| "#{idx + 1}. #{id}" }.join(" | ")
|
|
55
|
+
lines << %( // Load order: #{order_comment})
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
lines << "}"
|
|
59
|
+
lines.join("\n")
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def build_graph
|
|
65
|
+
@plugin_map.clear
|
|
66
|
+
@graph.clear
|
|
67
|
+
|
|
68
|
+
plugins.each do |klass|
|
|
69
|
+
next unless klass.respond_to?(:id)
|
|
70
|
+
|
|
71
|
+
identifier = klass.id
|
|
72
|
+
@plugin_map[identifier] = klass
|
|
73
|
+
@graph[identifier] ||= { dependencies: Set.new, dependents: Set.new }
|
|
74
|
+
|
|
75
|
+
deps = klass.respond_to?(:dependencies) ? Array(klass.dependencies) : []
|
|
76
|
+
deps.each do |dep|
|
|
77
|
+
dep_id = dep.to_s
|
|
78
|
+
@graph[identifier][:dependencies] << dep_id
|
|
79
|
+
@graph[dep_id] ||= { dependencies: Set.new, dependents: Set.new }
|
|
80
|
+
@graph[dep_id][:dependents] << identifier
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def validate_dependencies!
|
|
86
|
+
missing = {}
|
|
87
|
+
|
|
88
|
+
@graph.each do |identifier, meta|
|
|
89
|
+
meta[:dependencies].each do |dependency|
|
|
90
|
+
next if @plugin_map.key?(dependency)
|
|
91
|
+
|
|
92
|
+
missing[identifier] ||= []
|
|
93
|
+
missing[identifier] << dependency
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
return if missing.empty?
|
|
98
|
+
|
|
99
|
+
plugin_id, deps = missing.first
|
|
100
|
+
chain = deps.map { |dep| "#{plugin_id} → #{dep} (missing)" }
|
|
101
|
+
available = normalize_available
|
|
102
|
+
raise PluginDependencyError.missing(plugin_id: plugin_id, missing: deps, chain: chain, available: available)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def perform_topological_sort
|
|
106
|
+
indegree = Hash.new(0)
|
|
107
|
+
@graph.each_key { |identifier| indegree[identifier] ||= 0 }
|
|
108
|
+
|
|
109
|
+
@graph.each do |identifier, meta|
|
|
110
|
+
meta[:dependencies].each do |dependency|
|
|
111
|
+
next unless @plugin_map.key?(dependency)
|
|
112
|
+
|
|
113
|
+
indegree[identifier] += 1
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
queue = @plugin_map.values.select { |klass| indegree[klass.id].zero? }
|
|
118
|
+
queue.sort_by! { |klass| sort_signature(klass) }
|
|
119
|
+
|
|
120
|
+
sorted = []
|
|
121
|
+
until queue.empty?
|
|
122
|
+
current = queue.shift
|
|
123
|
+
sorted << current.id
|
|
124
|
+
|
|
125
|
+
dependents = Array(@graph.dig(current.id, :dependents))
|
|
126
|
+
dependents.each do |dependent|
|
|
127
|
+
next unless indegree.key?(dependent)
|
|
128
|
+
|
|
129
|
+
indegree[dependent] -= 1
|
|
130
|
+
next unless indegree[dependent].zero?
|
|
131
|
+
|
|
132
|
+
queue << @plugin_map[dependent]
|
|
133
|
+
queue.sort_by! { |klass| sort_signature(klass) }
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
if sorted.size != @plugin_map.size
|
|
138
|
+
raise PluginDependencyError.circular(cycle: build_cycle(sorted), available: normalize_available)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
sorted
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def sort_signature(klass)
|
|
145
|
+
priority_value = if klass.respond_to?(:priority_value)
|
|
146
|
+
-klass.priority_value
|
|
147
|
+
elsif klass.respond_to?(:priority) && klass.priority.respond_to?(:to_sym)
|
|
148
|
+
-Datalog::PluginSystem::PRIORITY_LEVELS.fetch(klass.priority.to_sym, 0)
|
|
149
|
+
else
|
|
150
|
+
0
|
|
151
|
+
end
|
|
152
|
+
[priority_value, klass.id.to_s]
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def build_cycle(sorted_identifiers)
|
|
156
|
+
remaining = @plugin_map.keys - sorted_identifiers
|
|
157
|
+
return [] if remaining.empty?
|
|
158
|
+
|
|
159
|
+
visited = {}
|
|
160
|
+
stack = []
|
|
161
|
+
|
|
162
|
+
remaining.each do |identifier|
|
|
163
|
+
cycle = explore_cycle(identifier, visited, stack, remaining)
|
|
164
|
+
return format_cycle(cycle) if cycle
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
[]
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def explore_cycle(node, visited, stack, remaining)
|
|
171
|
+
state = visited[node]
|
|
172
|
+
return nil if state == :done
|
|
173
|
+
|
|
174
|
+
if state == :active
|
|
175
|
+
cycle_start = stack.index(node) || 0
|
|
176
|
+
return stack[cycle_start..] + [node]
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
visited[node] = :active
|
|
180
|
+
stack << node
|
|
181
|
+
|
|
182
|
+
Array(@graph.dig(node, :dependencies)).each do |dependency|
|
|
183
|
+
next unless @plugin_map.key?(dependency)
|
|
184
|
+
next unless remaining.include?(dependency)
|
|
185
|
+
|
|
186
|
+
cycle = explore_cycle(dependency, visited, stack, remaining)
|
|
187
|
+
return cycle if cycle
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
stack.pop
|
|
191
|
+
visited[node] = :done
|
|
192
|
+
nil
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def format_cycle(nodes)
|
|
196
|
+
return [] unless Array(nodes).any?
|
|
197
|
+
|
|
198
|
+
edges = []
|
|
199
|
+
nodes.each_cons(2) do |from, to|
|
|
200
|
+
edges << "#{from} → #{to}"
|
|
201
|
+
end
|
|
202
|
+
edges << "#{nodes.last} → #{nodes.first}" if nodes.length > 1
|
|
203
|
+
edges
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def ensure_graph_built
|
|
207
|
+
build_graph if @graph.empty? && plugins.any?
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def normalize_available
|
|
211
|
+
if available_plugins.respond_to?(:keys)
|
|
212
|
+
available_plugins.keys
|
|
213
|
+
elsif available_plugins.respond_to?(:map)
|
|
214
|
+
available_plugins.map do |value|
|
|
215
|
+
value.respond_to?(:id) ? value.id : value
|
|
216
|
+
end
|
|
217
|
+
else
|
|
218
|
+
Array(available_plugins)
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
end
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "jekyll"
|
|
4
|
+
|
|
5
|
+
module Datalog
|
|
6
|
+
module PluginSystem
|
|
7
|
+
HOOKS = %i[before_render after_post_process custom_liquid_tags search_indexing].freeze
|
|
8
|
+
|
|
9
|
+
PRIORITY_LEVELS = {
|
|
10
|
+
highest: 40,
|
|
11
|
+
high: 30,
|
|
12
|
+
normal: 20,
|
|
13
|
+
low: 10,
|
|
14
|
+
lowest: 0
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
class PluginDependencyError < StandardError
|
|
18
|
+
attr_reader :plugin_id
|
|
19
|
+
|
|
20
|
+
def initialize(message, plugin_id: nil)
|
|
21
|
+
super(message)
|
|
22
|
+
@plugin_id = plugin_id
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.missing(plugin_id:, missing:, chain:, available: [])
|
|
26
|
+
missing_list = Array(missing)
|
|
27
|
+
message = "Cannot load '#{plugin_id}'\n"
|
|
28
|
+
descriptor = missing_list.size > 1 ? "Missing dependencies" : "Missing dependency"
|
|
29
|
+
message << " #{descriptor}: #{missing_list.map { |id| "'#{id}'" }.join(', ')}\n"
|
|
30
|
+
|
|
31
|
+
if Array(chain).any?
|
|
32
|
+
message << "\n Dependency chain:\n"
|
|
33
|
+
Array(chain).each do |step|
|
|
34
|
+
message << " #{step}\n"
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
message << "\n Available plugins: #{Array(available).map(&:to_s).sort.join(', ')}\n" if Array(available).any?
|
|
39
|
+
|
|
40
|
+
fix_line = if missing_list.size == 1
|
|
41
|
+
" To fix: Add '#{missing_list.first}' to enabled plugins in _config.yml"
|
|
42
|
+
else
|
|
43
|
+
joined = missing_list.map { |id| "'#{id}'" }.join(", ")
|
|
44
|
+
" To fix: Add #{joined} to enabled plugins in _config.yml"
|
|
45
|
+
end
|
|
46
|
+
message << "\n" << fix_line
|
|
47
|
+
|
|
48
|
+
new(message, plugin_id: plugin_id)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def self.circular(cycle:, available: [])
|
|
52
|
+
message = String.new("Circular dependency detected\n")
|
|
53
|
+
if Array(cycle).any?
|
|
54
|
+
message << "\n Cycle:\n"
|
|
55
|
+
Array(cycle).each do |step|
|
|
56
|
+
message << " #{step}\n"
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
message << "\n Available plugins: #{Array(available).map(&:to_s).sort.join(', ')}" if Array(available).any?
|
|
61
|
+
|
|
62
|
+
new(message)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
class << self
|
|
67
|
+
attr_accessor :active_plugins, :site
|
|
68
|
+
|
|
69
|
+
def available_plugins
|
|
70
|
+
@available_plugins ||= {}
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def register(klass)
|
|
74
|
+
identifier = klass.id
|
|
75
|
+
return unless identifier
|
|
76
|
+
|
|
77
|
+
available_plugins[identifier] = klass
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def activate!(site, plugin_instances)
|
|
81
|
+
self.site = site
|
|
82
|
+
self.active_plugins = plugin_instances
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def plugin(identifier)
|
|
86
|
+
Array(active_plugins).find { |instance| instance.id == identifier }
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def active_plugin_ids
|
|
90
|
+
Array(active_plugins).map(&:id)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def run_hook(hook, *args)
|
|
94
|
+
return unless HOOKS.include?(hook)
|
|
95
|
+
|
|
96
|
+
Array(active_plugins).each do |plugin|
|
|
97
|
+
next unless plugin.respond_to?(hook)
|
|
98
|
+
|
|
99
|
+
plugin.public_send(hook, *args)
|
|
100
|
+
rescue StandardError => e
|
|
101
|
+
Jekyll.logger.warn("datalog plugin", "#{plugin.id} failed during #{hook}: #{e.message}")
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def collect_hook(hook, *args)
|
|
106
|
+
return {} unless HOOKS.include?(hook)
|
|
107
|
+
|
|
108
|
+
Array(active_plugins).each_with_object({}) do |plugin, acc|
|
|
109
|
+
next unless plugin.respond_to?(hook)
|
|
110
|
+
|
|
111
|
+
result = plugin.public_send(hook, *args)
|
|
112
|
+
next unless result.is_a?(Hash)
|
|
113
|
+
|
|
114
|
+
acc.replace(deep_merge(acc, result))
|
|
115
|
+
rescue StandardError => e
|
|
116
|
+
Jekyll.logger.warn("datalog plugin", "#{plugin.id} failed during #{hook}: #{e.message}")
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
private
|
|
121
|
+
|
|
122
|
+
def deep_merge(left, right)
|
|
123
|
+
left.merge(right) do |_key, old_val, new_val|
|
|
124
|
+
if old_val.is_a?(Hash) && new_val.is_a?(Hash)
|
|
125
|
+
deep_merge(old_val, new_val)
|
|
126
|
+
elsif old_val.is_a?(Array) || new_val.is_a?(Array)
|
|
127
|
+
Array(old_val) + Array(new_val)
|
|
128
|
+
else
|
|
129
|
+
new_val
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
class Plugin
|
|
136
|
+
class << self
|
|
137
|
+
attr_writer :identifier
|
|
138
|
+
|
|
139
|
+
def depends_on(*plugin_ids)
|
|
140
|
+
return dependencies if plugin_ids.empty?
|
|
141
|
+
|
|
142
|
+
normalized = plugin_ids.flatten.compact.map(&:to_s).reject(&:empty?)
|
|
143
|
+
@dependencies = (dependencies + normalized).uniq
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def dependencies
|
|
147
|
+
@dependencies ||= []
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def priority(value = nil)
|
|
151
|
+
if value.nil?
|
|
152
|
+
@priority ||= :normal
|
|
153
|
+
else
|
|
154
|
+
symbol = value.to_sym
|
|
155
|
+
unless PRIORITY_LEVELS.key?(symbol)
|
|
156
|
+
raise ArgumentError, "Invalid plugin priority '#{value}'. Allowed: #{PRIORITY_LEVELS.keys.join(', ')}"
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
@priority = symbol
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def priority_value
|
|
164
|
+
PRIORITY_LEVELS.fetch(priority, PRIORITY_LEVELS[:normal])
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def id(value = nil)
|
|
168
|
+
if value
|
|
169
|
+
new_id = value.to_s
|
|
170
|
+
registry = Datalog::PluginSystem.available_plugins
|
|
171
|
+
previous_id = @identifier || default_identifier
|
|
172
|
+
|
|
173
|
+
registry.delete(previous_id) if previous_id && registry[previous_id] == self
|
|
174
|
+
|
|
175
|
+
@identifier = new_id
|
|
176
|
+
registry[new_id] = self
|
|
177
|
+
else
|
|
178
|
+
@identifier ||= default_identifier
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def inherited(subclass)
|
|
183
|
+
super
|
|
184
|
+
Datalog::PluginSystem.register(subclass)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
private
|
|
188
|
+
|
|
189
|
+
def default_identifier
|
|
190
|
+
raw = name&.split("::")&.last
|
|
191
|
+
raw ||= "plugin-#{object_id}"
|
|
192
|
+
|
|
193
|
+
raw.gsub(/([a-z\d])([A-Z])/, "\\1-\\2")
|
|
194
|
+
.tr("_", "-")
|
|
195
|
+
.downcase
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
attr_reader :site, :config
|
|
200
|
+
|
|
201
|
+
def initialize(site, config = {})
|
|
202
|
+
@site = site
|
|
203
|
+
@config = (config || {}).transform_keys(&:to_s)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def id
|
|
207
|
+
self.class.id
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def dependencies
|
|
211
|
+
self.class.dependencies
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def priority
|
|
215
|
+
self.class.priority
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def logger
|
|
219
|
+
@logger ||= Jekyll.logger
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def before_render(_document, _payload = nil); end
|
|
223
|
+
|
|
224
|
+
def after_post_process(_document); end
|
|
225
|
+
|
|
226
|
+
def custom_liquid_tags
|
|
227
|
+
{}
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def search_indexing(_document)
|
|
231
|
+
{}
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
require_relative "plugin_system/dependency_resolver"
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "shellwords"
|
|
4
|
+
|
|
5
|
+
module Datalog
|
|
6
|
+
module Plugins
|
|
7
|
+
class Citations < Datalog::PluginSystem::Plugin
|
|
8
|
+
id "datalog-citations"
|
|
9
|
+
depends_on "datalog-search"
|
|
10
|
+
priority :high
|
|
11
|
+
|
|
12
|
+
class CiteTag < Liquid::Tag
|
|
13
|
+
def initialize(tag_name, markup, tokens)
|
|
14
|
+
super
|
|
15
|
+
args = Shellwords.split(markup)
|
|
16
|
+
@citation_key = args.shift
|
|
17
|
+
@label = args.shift
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def render(context)
|
|
21
|
+
plugin = Datalog::PluginSystem.plugin("datalog-citations")
|
|
22
|
+
return missing_placeholder unless plugin
|
|
23
|
+
|
|
24
|
+
plugin.render_citation(context, @citation_key, @label)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def missing_placeholder
|
|
30
|
+
"<span class=\"datalog-citation datalog-citation--missing\">[citation]</span>"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
class BibliographyTag < Liquid::Tag
|
|
35
|
+
def render(context)
|
|
36
|
+
plugin = Datalog::PluginSystem.plugin("datalog-citations")
|
|
37
|
+
return "" unless plugin
|
|
38
|
+
|
|
39
|
+
plugin.render_bibliography(context)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def initialize(site, config = {})
|
|
44
|
+
super
|
|
45
|
+
@state = {}
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def before_render(document, _payload = nil)
|
|
49
|
+
citations = collect_citations(document)
|
|
50
|
+
state = ensure_state(document)
|
|
51
|
+
state[:lookup] = citations
|
|
52
|
+
state[:order] = []
|
|
53
|
+
document.data["datalog_citations"] = citations
|
|
54
|
+
document.data["datalog_bibliography"] = citations.values
|
|
55
|
+
document.data["datalog_citation_state"] = state[:key]
|
|
56
|
+
return unless config["bibliography_title"]
|
|
57
|
+
|
|
58
|
+
document.data["datalog_bibliography_title"] = config["bibliography_title"]
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def custom_liquid_tags
|
|
62
|
+
{
|
|
63
|
+
"datalog_cite" => CiteTag,
|
|
64
|
+
"datalog_bibliography" => BibliographyTag
|
|
65
|
+
}
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def search_indexing(document)
|
|
69
|
+
state = resolve_state(document)
|
|
70
|
+
return {} unless state && state[:lookup]&.any?
|
|
71
|
+
|
|
72
|
+
{
|
|
73
|
+
"citations" => {
|
|
74
|
+
"count" => state[:lookup].size,
|
|
75
|
+
"keys" => state[:lookup].keys.sort
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def render_citation(context, key, label)
|
|
81
|
+
return missing_markup unless key
|
|
82
|
+
|
|
83
|
+
state = state_from_context(context)
|
|
84
|
+
return missing_markup unless state
|
|
85
|
+
|
|
86
|
+
normalized_key = key.to_s
|
|
87
|
+
entry = state[:lookup][normalized_key]
|
|
88
|
+
return missing_markup unless entry
|
|
89
|
+
|
|
90
|
+
state[:order] << normalized_key unless state[:order].include?(normalized_key)
|
|
91
|
+
index = state[:order].index(normalized_key) + 1
|
|
92
|
+
text = label || entry[:label] || "[#{index}]"
|
|
93
|
+
text = text.gsub("%{n}", index.to_s)
|
|
94
|
+
|
|
95
|
+
if entry[:url]
|
|
96
|
+
%(<a class="datalog-citation" href="#{entry[:url]}" rel="noopener" data-citation="#{normalized_key}">#{text}</a>)
|
|
97
|
+
else
|
|
98
|
+
%(<span class="datalog-citation" data-citation="#{normalized_key}">#{text}</span>)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def render_bibliography(context)
|
|
103
|
+
state = state_from_context(context)
|
|
104
|
+
return "" unless state && state[:lookup]&.any?
|
|
105
|
+
|
|
106
|
+
entries = state[:order].map { |key| state[:lookup][key] }.compact
|
|
107
|
+
entries = state[:lookup].values if entries.empty?
|
|
108
|
+
|
|
109
|
+
items = entries.each_with_index.map do |entry, idx|
|
|
110
|
+
anchor = entry[:id] || "citation-#{idx + 1}"
|
|
111
|
+
%(<li id="cite-#{anchor}">#{entry[:formatted]}</li>)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
%(<ol class="datalog-bibliography">#{items.join}</ol>)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
private
|
|
118
|
+
|
|
119
|
+
def collect_citations(document)
|
|
120
|
+
entries = Array(document.data["citations"] || document.data["references"])
|
|
121
|
+
global = Array(site.data.dig("citations", document.data["citation_group"]))
|
|
122
|
+
entries += global if global.any?
|
|
123
|
+
|
|
124
|
+
entries.each_with_object({}) do |entry, acc|
|
|
125
|
+
normalized = normalize_entry(entry)
|
|
126
|
+
next unless normalized[:id]
|
|
127
|
+
|
|
128
|
+
acc[normalized[:id]] = normalized
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def normalize_entry(entry)
|
|
133
|
+
case entry
|
|
134
|
+
when String
|
|
135
|
+
identifier = entry.downcase.gsub(/[^a-z0-9]+/, "-").gsub(/^-|-$/, "")
|
|
136
|
+
{ id: identifier, title: entry, formatted: entry }
|
|
137
|
+
when Hash
|
|
138
|
+
normalized = entry.transform_keys(&:to_sym)
|
|
139
|
+
normalized[:id] = normalized[:id].to_s if normalized[:id]
|
|
140
|
+
normalized[:formatted] ||= build_formatted_entry(normalized)
|
|
141
|
+
normalized
|
|
142
|
+
else
|
|
143
|
+
{}
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def build_formatted_entry(entry)
|
|
148
|
+
authors = Array(entry[:authors] || entry[:author]).join(", ")
|
|
149
|
+
year = entry[:year]
|
|
150
|
+
title = entry[:title]
|
|
151
|
+
container = entry[:journal] || entry[:conference] || entry[:publisher]
|
|
152
|
+
segments = []
|
|
153
|
+
segments << authors unless authors.empty?
|
|
154
|
+
segments << "(#{year})" if year
|
|
155
|
+
segments << "<em>#{title}</em>" if title
|
|
156
|
+
segments << container if container
|
|
157
|
+
segments << entry[:url] if entry[:url] && !entry[:url].empty?
|
|
158
|
+
segments.compact.join(". ")
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def ensure_state(document)
|
|
162
|
+
key = document.relative_path || document.path
|
|
163
|
+
@state[key] = { key: key }.merge(@state[key] || {})
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def resolve_state(document)
|
|
167
|
+
key = document.data["datalog_citation_state"] || document.relative_path || document.path
|
|
168
|
+
@state[key]
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def state_from_context(context)
|
|
172
|
+
page = context.registers[:page] || {}
|
|
173
|
+
key = page["datalog_citation_state"]
|
|
174
|
+
@state[key]
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def missing_markup
|
|
178
|
+
"<span class=\"datalog-citation datalog-citation--missing\">[citation]</span>"
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|