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.
Files changed (180) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +162 -0
  3. data/CITATION.cff +29 -0
  4. data/LICENSE +21 -0
  5. data/README.md +257 -0
  6. data/_data/academic.yml +217 -0
  7. data/_data/cdn-integrity.yml +51 -0
  8. data/_data/config/author.yml +121 -0
  9. data/_data/config/features.yml +262 -0
  10. data/_data/config/site.yml +42 -0
  11. data/_data/config/theme.yml +181 -0
  12. data/_data/datasets.yml +28 -0
  13. data/_data/i18n/en.yml +438 -0
  14. data/_data/i18n/es.yml +438 -0
  15. data/_data/i18n/pt.yml +438 -0
  16. data/_data/js_manifest.json +22 -0
  17. data/_data/js_meta.json +371 -0
  18. data/_data/navigation.yml +145 -0
  19. data/_data/projects.yml +41 -0
  20. data/_data/publications.yml +28 -0
  21. data/_data/social.yml +73 -0
  22. data/_data/visualizations.yml +51 -0
  23. data/_includes/analytics/dashboard.html +149 -0
  24. data/_includes/analytics.html +9 -0
  25. data/_includes/components/academic-dashboard.html +347 -0
  26. data/_includes/components/advanced-search.html +682 -0
  27. data/_includes/components/api-function.html +94 -0
  28. data/_includes/components/author-bio.html +166 -0
  29. data/_includes/components/bookmark-system.html +96 -0
  30. data/_includes/components/breadcrumbs.html +89 -0
  31. data/_includes/components/citation-tools.html +94 -0
  32. data/_includes/components/comments.html +244 -0
  33. data/_includes/components/content-provenance.html +43 -0
  34. data/_includes/components/content-recommendations.html +228 -0
  35. data/_includes/components/difficulty-badge.html +84 -0
  36. data/_includes/components/email-preferences.html +200 -0
  37. data/_includes/components/enhanced-code-block.html +212 -0
  38. data/_includes/components/enhanced-metadata.html +228 -0
  39. data/_includes/components/enhanced-toc.html +209 -0
  40. data/_includes/components/hero.html +15 -0
  41. data/_includes/components/language-switcher.html +396 -0
  42. data/_includes/components/math-fallback.html +19 -0
  43. data/_includes/components/navigation-enhancements.html +454 -0
  44. data/_includes/components/newsletter-signup.html +178 -0
  45. data/_includes/components/open-science-badges.html +16 -0
  46. data/_includes/components/package-install.html +194 -0
  47. data/_includes/components/performance-monitor.html +170 -0
  48. data/_includes/components/popular-posts.html +233 -0
  49. data/_includes/components/post-hero.html +62 -0
  50. data/_includes/components/reading-progress.html +133 -0
  51. data/_includes/components/reading-time.html +121 -0
  52. data/_includes/components/responsive-image.html +63 -0
  53. data/_includes/components/search-facets.html +307 -0
  54. data/_includes/components/series-navigation.html +124 -0
  55. data/_includes/components/social-proof.html +34 -0
  56. data/_includes/components/social-share.html +119 -0
  57. data/_includes/components/user-preferences.html +566 -0
  58. data/_includes/components/visualization-card.html +24 -0
  59. data/_includes/components/viz-table-fallback.html +19 -0
  60. data/_includes/critical-css/default.html +0 -0
  61. data/_includes/critical-css/home.html +0 -0
  62. data/_includes/critical-css/post.html +0 -0
  63. data/_includes/csp-meta.html +17 -0
  64. data/_includes/footer/nav-column.html +31 -0
  65. data/_includes/footer.html +76 -0
  66. data/_includes/head.html +337 -0
  67. data/_includes/header/navigation.html +48 -0
  68. data/_includes/header.html +70 -0
  69. data/_includes/helpers/array-to-sentence.html +21 -0
  70. data/_includes/helpers/date-format.html +16 -0
  71. data/_includes/helpers/link-with-icon.html +26 -0
  72. data/_includes/layouts/default/article.html +67 -0
  73. data/_includes/meta/language-attributes.html +25 -0
  74. data/_includes/meta/math-config.html +21 -0
  75. data/_includes/meta/schema.html +153 -0
  76. data/_includes/meta/scripts-loader.html +52 -0
  77. data/_includes/post/related-posts.html +63 -0
  78. data/_includes/scripts.html +72 -0
  79. data/_includes/skip-link.html +1 -0
  80. data/_includes/toc.html +2 -0
  81. data/_layouts/archive.html +282 -0
  82. data/_layouts/dataset.html +52 -0
  83. data/_layouts/default.html +33 -0
  84. data/_layouts/home.html +48 -0
  85. data/_layouts/notebook.html +173 -0
  86. data/_layouts/package.html +238 -0
  87. data/_layouts/page.html +16 -0
  88. data/_layouts/portfolio.html +41 -0
  89. data/_layouts/post-sidebar.html +183 -0
  90. data/_layouts/post.html +338 -0
  91. data/_layouts/project.html +252 -0
  92. data/_layouts/research.html +504 -0
  93. data/_plugins/analytics_dashboard.rb +315 -0
  94. data/_plugins/config_validator.rb +397 -0
  95. data/_plugins/csp_generator.rb +115 -0
  96. data/_plugins/datalog_bibliography.rb +19 -0
  97. data/_plugins/datalog_comments.rb +18 -0
  98. data/_plugins/datalog_slides.rb +20 -0
  99. data/_plugins/front_matter_compat.rb +109 -0
  100. data/_plugins/i18n.rb +160 -0
  101. data/_plugins/image_optimizer.rb +445 -0
  102. data/_plugins/math_preprocessor.rb +185 -0
  103. data/_plugins/notebook_converter.rb +1107 -0
  104. data/_plugins/plugin_loader.rb +185 -0
  105. data/_plugins/publications_generator.rb +234 -0
  106. data/_plugins/reading_time.rb +15 -0
  107. data/_plugins/search_normalizer.rb +74 -0
  108. data/_plugins/sri_filter.rb +18 -0
  109. data/_plugins/toc_filter.rb +68 -0
  110. data/_plugins/warning_filter.rb +3 -0
  111. data/_sass/_base.scss +142 -0
  112. data/_sass/_components.scss +2380 -0
  113. data/_sass/_header.scss +470 -0
  114. data/_sass/_interactive.scss +344 -0
  115. data/_sass/_layout.scss +1760 -0
  116. data/_sass/_mathematical.scss +368 -0
  117. data/_sass/_mixins.scss +99 -0
  118. data/_sass/_package-docs.scss +715 -0
  119. data/_sass/_phase1-enhancements.scss +721 -0
  120. data/_sass/_phase3-enhancements.scss +874 -0
  121. data/_sass/_phase4-enhancements.scss +1214 -0
  122. data/_sass/_phase5-enhancements.scss +414 -0
  123. data/_sass/_search.scss +654 -0
  124. data/_sass/_syntax-highlighting.scss +128 -0
  125. data/_sass/_theme.scss +79 -0
  126. data/_sass/_typography.scss +148 -0
  127. data/_sass/_utilities.scss +334 -0
  128. data/_sass/_variables.scss +153 -0
  129. data/_sass/_visualizations.scss +242 -0
  130. data/assets/css/main.scss +4 -0
  131. data/assets/img/20220607123041_detail.001.png +0 -0
  132. data/assets/img/favicons/android-chrome-192x192.png +0 -0
  133. data/assets/img/favicons/android-chrome-512x512.png +0 -0
  134. data/assets/img/favicons/apple-touch-icon.png +0 -0
  135. data/assets/img/favicons/favicon-16x16.png +0 -0
  136. data/assets/img/favicons/favicon-32x32.png +0 -0
  137. data/assets/img/favicons/favicon.ico +0 -0
  138. data/assets/img/favicons/site.webmanifest +21 -0
  139. data/assets/img/portfolio-placeholder.svg +20 -0
  140. data/assets/js/academic.js +262 -0
  141. data/assets/js/analytics-dashboard.js +382 -0
  142. data/assets/js/core/dark-mode.js +79 -0
  143. data/assets/js/core/github-cards.js +123 -0
  144. data/assets/js/core/language-filter.js +69 -0
  145. data/assets/js/core/navigation.js +184 -0
  146. data/assets/js/core/scroll-progress.js +45 -0
  147. data/assets/js/core/search-hotkeys.js +62 -0
  148. data/assets/js/core/skip-links.js +62 -0
  149. data/assets/js/loader.js +163 -0
  150. data/assets/js/main.js +23 -0
  151. data/assets/js/math.js +818 -0
  152. data/assets/js/notebook.js +158 -0
  153. data/assets/js/search/analytics.js +91 -0
  154. data/assets/js/search/app.js +271 -0
  155. data/assets/js/search/autocomplete.js +120 -0
  156. data/assets/js/search/engine.js +260 -0
  157. data/assets/js/search/filters.js +38 -0
  158. data/assets/js/search/render.js +217 -0
  159. data/assets/js/search/utils.js +99 -0
  160. data/assets/js/search.js +354 -0
  161. data/assets/js/visualizations.js +816 -0
  162. data/assets/publications/datalog-publications.bib +8 -0
  163. data/assets/publications/datalog-publications.ris +9 -0
  164. data/assets/publications/publications.bib +30 -0
  165. data/assets/templates/diogo-ribeiro-cv.md +31 -0
  166. data/assets/templates/diogo-ribeiro-cv.tex +32 -0
  167. data/bin/datalog +26 -0
  168. data/datalog-theme.gemspec +72 -0
  169. data/lib/datalog/cli.rb +563 -0
  170. data/lib/datalog/plugin_system/dependency_resolver.rb +223 -0
  171. data/lib/datalog/plugin_system.rb +237 -0
  172. data/lib/datalog/plugins/citations.rb +182 -0
  173. data/lib/datalog/plugins/comments.rb +183 -0
  174. data/lib/datalog/plugins/search.rb +45 -0
  175. data/lib/datalog/plugins/slides.rb +121 -0
  176. data/lib/datalog/theme/theme.rb +18 -0
  177. data/lib/datalog/theme/version.rb +7 -0
  178. data/lib/datalog/warning_filter.rb +28 -0
  179. data/lib/datalog-theme.rb +17 -0
  180. 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