datalog-theme 0.10.1 → 0.11.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 (130) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +127 -0
  3. data/CITATION.cff +2 -2
  4. data/README.md +4 -2
  5. data/_data/i18n/en.yml +55 -3
  6. data/_data/i18n/es.yml +55 -3
  7. data/_data/i18n/pt.yml +55 -3
  8. data/_includes/analytics/dashboard.html +24 -9
  9. data/_includes/components/academic-dashboard.html +168 -133
  10. data/_includes/components/author-bio.html +7 -5
  11. data/_includes/components/author-list.html +6 -5
  12. data/_includes/components/citation-tools.html +2 -1
  13. data/_includes/components/content-provenance.html +19 -1
  14. data/_includes/components/correction-report.html +15 -8
  15. data/_includes/components/hero.html +22 -15
  16. data/_includes/components/package-index.html +121 -0
  17. data/_includes/components/package-install.html +27 -50
  18. data/_includes/components/post-list.html +14 -0
  19. data/_includes/components/responsive-image.html +15 -21
  20. data/_includes/footer.html +1 -1
  21. data/_includes/head.html +70 -11
  22. data/_includes/layouts/default/article.html +76 -46
  23. data/_includes/meta/dataset-json.html +171 -0
  24. data/_includes/meta/math-config.html +15 -1
  25. data/_includes/meta/package-json.html +69 -0
  26. data/_includes/meta/person-json.html +36 -13
  27. data/_includes/meta/publisher.html +15 -0
  28. data/_includes/meta/schema.html +108 -23
  29. data/_includes/meta/scholarly.html +10 -5
  30. data/_includes/post/related-posts.html +17 -43
  31. data/_includes/scripts.html +2 -2
  32. data/_includes/search/index-data.json +10 -1
  33. data/_includes/search/page.html +11 -6
  34. data/_layouts/archive.html +67 -0
  35. data/_layouts/dataset.html +2 -1
  36. data/_layouts/default.html +13 -11
  37. data/_layouts/docs.html +53 -0
  38. data/_layouts/home.html +30 -1
  39. data/_layouts/notebook.html +5 -1
  40. data/_layouts/package.html +38 -21
  41. data/_layouts/portfolio.html +1 -0
  42. data/_layouts/post.html +21 -62
  43. data/_layouts/project.html +2 -1
  44. data/_layouts/research.html +14 -2
  45. data/_plugins/analytics_dashboard.rb +4 -1
  46. data/_plugins/archive.rb +114 -0
  47. data/_plugins/authors.rb +50 -1
  48. data/_plugins/citation_exports.rb +12 -1
  49. data/_plugins/config_validator.rb +77 -2
  50. data/_plugins/correction_fallback.rb +110 -0
  51. data/_plugins/image_optimizer.rb +129 -22
  52. data/_plugins/math_preprocessor.rb +7 -48
  53. data/_plugins/notebook_converter.rb +44 -5
  54. data/_plugins/packages.rb +84 -0
  55. data/_plugins/page_dates.rb +37 -0
  56. data/_plugins/references.rb +16 -2
  57. data/_plugins/related_posts.rb +149 -0
  58. data/_plugins/search_sections.rb +110 -0
  59. data/_plugins/site_identity.rb +42 -0
  60. data/_plugins/social_cards.rb +17 -0
  61. data/_sass/_academic-dashboard.scss +18 -30
  62. data/_sass/_base.scss +2 -0
  63. data/_sass/_components.scss +21 -23
  64. data/_sass/_docs.scss +143 -0
  65. data/_sass/_features.scss +6 -0
  66. data/_sass/_layout.scss +65 -6
  67. data/_sass/_mathematical.scss +61 -19
  68. data/_sass/_notebooks.scss +1 -32
  69. data/_sass/_package-docs.scss +6 -9
  70. data/_sass/_post-components.scss +23 -23
  71. data/_sass/_print.scss +1 -1
  72. data/_sass/_search-page.scss +75 -0
  73. data/_sass/_search.scss +17 -26
  74. data/_sass/_syntax-highlighting.scss +23 -1
  75. data/_sass/_theme.scss +1 -0
  76. data/_sass/_utilities.scss +87 -40
  77. data/_sass/_variables.scss +31 -29
  78. data/assets/css/main.scss +2 -1
  79. data/assets/js/dist/academic.js +1 -1
  80. data/assets/js/dist/analytics-dashboard.js +1 -1
  81. data/assets/js/dist/chunks/chunk-TNVD6UAM.js +1 -0
  82. data/assets/js/dist/comments.js +1 -1
  83. data/assets/js/dist/contact.js +1 -1
  84. data/assets/js/dist/core.js +1 -1
  85. data/assets/js/dist/corrections.js +5 -1
  86. data/assets/js/dist/loader.js +1 -1
  87. data/assets/js/dist/math.js +2 -1
  88. data/assets/js/dist/moderation.js +1 -1
  89. data/assets/js/dist/reactions.js +1 -1
  90. data/assets/js/dist/search.js +1 -1
  91. data/assets/js/dist/sources.json +20 -16
  92. data/assets/js/dist/subscriptions.js +1 -1
  93. data/assets/js/loader.js +6 -2
  94. data/lib/datalog/audit/checks.rb +269 -0
  95. data/lib/datalog/audit/known_keys.rb +61 -0
  96. data/lib/datalog/audit/site_reader.rb +118 -0
  97. data/lib/datalog/audit/source_file.rb +85 -0
  98. data/lib/datalog/audit.rb +145 -0
  99. data/lib/datalog/citations/bibtex.rb +200 -0
  100. data/lib/datalog/citations/entry.rb +333 -0
  101. data/lib/datalog/citations/markup.rb +79 -0
  102. data/lib/datalog/cli.rb +164 -89
  103. data/lib/datalog/critical_css.rb +126 -21
  104. data/lib/datalog/latex_speech/words.json +88 -0
  105. data/lib/datalog/latex_speech.rb +355 -0
  106. data/lib/datalog/packages/command.rb +51 -0
  107. data/lib/datalog/packages/refresh.rb +187 -0
  108. data/lib/datalog/packages.rb +177 -0
  109. data/lib/datalog/plugin_system.rb +5 -0
  110. data/lib/datalog/plugins/citations.rb +325 -109
  111. data/lib/datalog/site_config.rb +26 -0
  112. data/lib/datalog/social_cards/font.rb +286 -0
  113. data/lib/datalog/social_cards/fonts/IBMPlexSans-Regular.ttf +0 -0
  114. data/lib/datalog/social_cards/fonts/IBMPlexSerif-SemiBold.ttf +0 -0
  115. data/lib/datalog/social_cards/fonts/OFL.txt +92 -0
  116. data/lib/datalog/social_cards/geometry.rb +51 -0
  117. data/lib/datalog/social_cards/outline.rb +54 -0
  118. data/lib/datalog/social_cards/plain_text.rb +258 -0
  119. data/lib/datalog/social_cards/template.rb +322 -0
  120. data/lib/datalog/social_cards/template.svg +17 -0
  121. data/lib/datalog/social_cards/typesetter.rb +178 -0
  122. data/lib/datalog/social_cards.rb +369 -0
  123. data/lib/datalog/theme/updater.rb +307 -0
  124. data/lib/datalog/theme/version.rb +1 -1
  125. metadata +44 -7
  126. data/_includes/components/enhanced-code-block.html +0 -212
  127. data/_includes/components/performance-monitor.html +0 -170
  128. data/_includes/components/viz-table-fallback.html +0 -19
  129. data/_plugins/datalog_bibliography.rb +0 -21
  130. data/assets/js/dist/chunks/chunk-WIRUK3OZ.js +0 -1
@@ -1,182 +1,398 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "shellwords"
3
+ require "cgi"
4
+ require "json"
5
+ require_relative "../citations/entry"
6
+ require_relative "../citations/markup"
4
7
 
5
8
  module Datalog
6
9
  module Plugins
10
+ # In-text citations and the bibliography they build, on a par with the
11
+ # figure, table and statement references of _plugins/references.rb:
12
+ #
13
+ # bibliography: _bibliography/missing-data.bib # or .json (CSL-JSON), or a list
14
+ #
15
+ # Multiple imputation {% cite rubin1987 %} and its critics {% cite allison2001 vanbuuren2018 p="12" %}.
16
+ #
17
+ # The tags write placeholders. Once the page's Markdown is converted, every
18
+ # citation gets its number in the order it appears (numeric) or its author
19
+ # and year (author-year), each linked to its entry, and the bibliography
20
+ # lists the cited works with links back to where each was cited. A key the
21
+ # page's sources do not have, or a key two entries share, stops the build.
22
+ #
23
+ # Entries come from the page's `bibliography` files (else the site's
24
+ # `citations.bibliography`), its `citations:` list, a `references:` list of
25
+ # maps, and `citation_group` in _data/citations.yml. `nocite: [key]` or
26
+ # `nocite: all` lists works the text does not cite.
7
27
  class Citations < Datalog::PluginSystem::Plugin
8
28
  id "datalog-citations"
9
- depends_on "datalog-search"
10
29
  priority :high
11
30
 
31
+ Markup = Datalog::Citations::Markup
32
+ STYLES = %w[numeric author-year].freeze
33
+ LOCATORS = %w[p pp chap sec loc].freeze
34
+ KEY = /\A[^\s"'<>&{}%]+\z/
35
+ MARKER = %r{<span class="datalog-cite" data-cite="([^"]*)"(?: data-cite-locator="([^"]*)")?></span>}
36
+ SOURCE_TAG = /\{%-?\s*(?:cite|datalog_cite)\s+([^%]*?)-?%\}/
37
+
38
+ # {% cite key %}, {% cite key1 key2 %}, {% cite key p="12" %}.
12
39
  class CiteTag < Liquid::Tag
13
- def initialize(tag_name, markup, tokens)
40
+ def initialize(tag_name, markup, options)
14
41
  super
15
- args = Shellwords.split(markup)
16
- @citation_key = args.shift
17
- @label = args.shift
42
+ @keys, @locator = Citations.parse_markup(markup, tag_name)
43
+ end
44
+
45
+ def render(context)
46
+ unless Datalog::PluginSystem.plugin("datalog-citations")
47
+ page = context.registers[:page] || {}
48
+ raise Liquid::ArgumentError,
49
+ "#{page['path']} uses {% #{tag_name} %}, which needs the datalog-citations plugin: " \
50
+ "add it to datalog_plugins.enabled in _config.yml"
51
+ end
52
+
53
+ locator = @locator ? %( data-cite-locator="#{CGI.escapeHTML(@locator)}") : ""
54
+ %(<span class="datalog-cite" data-cite="#{CGI.escapeHTML(@keys.join(' '))}"#{locator}></span>)
18
55
  end
56
+ end
19
57
 
58
+ # The page's bibliography, where a layout puts it.
59
+ class BibliographyTag < Liquid::Tag
20
60
  def render(context)
61
+ page = context.registers[:page] || {}
21
62
  plugin = Datalog::PluginSystem.plugin("datalog-citations")
22
- return missing_placeholder unless plugin
63
+ return plugin.bibliography_html(page) if plugin
23
64
 
24
- plugin.render_citation(context, @citation_key, @label)
65
+ if page["datalog_bibliography"]
66
+ Jekyll.logger.warn("datalog-citations", "#{page['path']} sets datalog_bibliography, but " \
67
+ "datalog_plugins.enabled does not list datalog-citations")
68
+ end
69
+ ""
25
70
  end
71
+ end
26
72
 
27
- private
73
+ # [keys, "p|12"] from `key1 key2 p="12"`.
74
+ def self.parse_markup(markup, tag_name = "cite")
75
+ keys = []
76
+ locator = nil
77
+ tokens(markup).each do |token|
78
+ name, value = token.split("=", 2)
79
+ if value.nil?
80
+ keys << check_key(token, tag_name)
81
+ else
82
+ unless LOCATORS.include?(name)
83
+ raise Liquid::ArgumentError, "{% #{tag_name} %} takes a locator as p=, pp=, chap=, sec= or loc=; " \
84
+ "got #{name}="
85
+ end
86
+
87
+ locator = "#{name}|#{unquote(value)}"
88
+ end
89
+ end
90
+ raise Liquid::ArgumentError, "{% #{tag_name} %} needs at least one citation key" if keys.empty?
28
91
 
29
- def missing_placeholder
30
- "<span class=\"datalog-citation datalog-citation--missing\">[citation]</span>"
92
+ [keys.uniq, locator]
93
+ end
94
+
95
+ # The markup split at spaces outside quotes, character by character: a
96
+ # regular expression over it could take quadratic time on a long word.
97
+ def self.tokens(markup)
98
+ tokens = []
99
+ current = +""
100
+ quote = nil
101
+ markup.to_s.each_char do |char|
102
+ if quote
103
+ quote = nil if char == quote
104
+ elsif ['"', "'"].include?(char)
105
+ quote = char
106
+ elsif char.strip.empty?
107
+ tokens << current unless current.empty?
108
+ current = +""
109
+ next
110
+ end
111
+ current << char
31
112
  end
113
+ tokens << current unless current.empty?
114
+ tokens
32
115
  end
33
116
 
34
- class BibliographyTag < Liquid::Tag
35
- def render(context)
36
- plugin = Datalog::PluginSystem.plugin("datalog-citations")
37
- return "" unless plugin
117
+ def self.unquote(value)
118
+ quoted = value.length >= 2 && ['"', "'"].include?(value[0]) && value[-1] == value[0]
119
+ quoted ? value[1..-2] : value
120
+ end
38
121
 
39
- plugin.render_bibliography(context)
40
- end
122
+ def self.check_key(key, tag_name)
123
+ return key if key.match?(KEY)
124
+
125
+ raise Liquid::ArgumentError, "{% #{tag_name} %} takes citation keys without spaces, quotes, <, >, " \
126
+ "&, braces or %; got #{key.inspect}"
41
127
  end
42
128
 
43
129
  def initialize(site, config = {})
44
130
  super
45
131
  @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"]
132
+ @files = {}
59
133
  end
60
134
 
61
135
  def custom_liquid_tags
62
136
  {
137
+ "cite" => CiteTag,
63
138
  "datalog_cite" => CiteTag,
64
139
  "datalog_bibliography" => BibliographyTag
65
140
  }
66
141
  end
67
142
 
68
- def search_indexing(document)
69
- state = resolve_state(document)
70
- return {} unless state && state[:lookup]&.any?
143
+ # ------------------------------------------------------------- sources
71
144
 
72
- {
73
- "citations" => {
74
- "count" => state[:lookup].size,
75
- "keys" => state[:lookup].keys.sort
76
- }
77
- }
145
+ def before_render(document, payload = nil)
146
+ state = load_state(document)
147
+ # A page's `page` in Liquid is a copy of its data taken before it
148
+ # renders (a document's reads its data live), so what numbering finds
149
+ # has to reach that copy too.
150
+ state[:page] = payload["page"] if payload.respond_to?(:[]) && payload["page"].is_a?(Hash)
151
+ publish(document, state, "datalog_citation_state" => state[:id])
152
+ title = config["bibliography_title"]
153
+ return unless title && !document.data["datalog_bibliography_title"]
154
+
155
+ publish(document, state, "datalog_bibliography_title" => title)
78
156
  end
79
157
 
80
- def render_citation(context, key, label)
81
- return missing_markup unless key
158
+ def publish(document, state, values)
159
+ document.data.merge!(values)
160
+ state[:page]&.merge!(values)
161
+ end
82
162
 
83
- state = state_from_context(context)
84
- return missing_markup unless state
163
+ def load_state(document)
164
+ id = document.relative_path || document.path
165
+ return @state[id] if @state.key?(id)
85
166
 
86
- normalized_key = key.to_s
87
- entry = state[:lookup][normalized_key]
88
- return missing_markup unless entry
167
+ entries = {}
168
+ sources(document).each do |label, items|
169
+ items.each do |entry|
170
+ next if entry.key.empty?
89
171
 
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)
172
+ if entries.key?(entry.key)
173
+ raise Jekyll::Errors::FatalException,
174
+ "#{document.relative_path} has two bibliography entries with the key \"#{entry.key}\" " \
175
+ "(in #{entries[entry.key][:source]} and #{label}); each key has to be unique"
176
+ end
94
177
 
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>)
178
+ entries[entry.key] = { entry: entry, source: label }
179
+ end
99
180
  end
181
+ @state[id] = { id: id, entries: entries.transform_values { |value| value[:entry] },
182
+ sources: sources_label(document) }
100
183
  end
101
184
 
102
- def render_bibliography(context)
103
- state = state_from_context(context)
104
- return "" unless state && state[:lookup]&.any?
185
+ # [label, [Entry, ...]] for each place the page's entries come from.
186
+ def sources(document)
187
+ data = document.data
188
+ found = bibliography_files(document).map { |path| [path, read_file(document, path)] }
189
+ found << ["citations:", front_matter_entries(data["citations"])]
190
+ # The research layout's `references:` is a hand-written list of strings;
191
+ # only a list of maps is a list of entries.
192
+ found << ["references:", front_matter_entries(Array(data["references"]).grep(Hash))]
193
+ group = data["citation_group"]
194
+ if group
195
+ found << ["_data/citations.yml (#{group})", front_matter_entries(site.data.dig("citations", group.to_s))]
196
+ end
197
+ found
198
+ end
105
199
 
106
- entries = state[:order].map { |key| state[:lookup][key] }.compact
107
- entries = state[:lookup].values if entries.empty?
200
+ def front_matter_entries(items)
201
+ Array(items).map { |item| Datalog::Citations::Entry.from_front_matter(item) }
202
+ end
108
203
 
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
204
+ def sources_label(document)
205
+ labels = bibliography_files(document)
206
+ labels << "citations:" if document.data["citations"]
207
+ labels.empty? ? "it has none" : labels.join(", ")
208
+ end
209
+
210
+ def bibliography_files(document)
211
+ own = document.data["bibliography"]
212
+ value = own.nil? ? site_setting("bibliography") : own
213
+ return [] if value == false
113
214
 
114
- %(<ol class="datalog-bibliography">#{items.join}</ol>)
215
+ Array(value).map(&:to_s).reject(&:empty?)
115
216
  end
116
217
 
117
- private
218
+ def read_file(document, path)
219
+ file = resolve(document, path)
220
+ stamp = [File.mtime(file), File.size(file)]
221
+ @files.delete(file) unless @files.dig(file, :stamp) == stamp
222
+ @files[file] ||= { stamp: stamp, entries: parse_file(document, file, path) }
223
+ @files[file][:entries]
224
+ end
118
225
 
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?
226
+ # A path in the site: from its source directory, or from the page's own.
227
+ def resolve(document, path)
228
+ source = File.expand_path(site.source)
229
+ base = File.dirname(File.expand_path(document.path.to_s, source))
230
+ candidates = [File.expand_path(path, source), File.expand_path(path, base)]
231
+ file = candidates.find { |candidate| candidate.start_with?("#{source}/") && File.file?(candidate) }
232
+ return file if file
123
233
 
124
- entries.each_with_object({}) do |entry, acc|
125
- normalized = normalize_entry(entry)
126
- next unless normalized[:id]
234
+ raise Jekyll::Errors::FatalException,
235
+ "#{document.relative_path} names the bibliography #{path}, which is not a file in the site"
236
+ end
127
237
 
128
- acc[normalized[:id]] = normalized
238
+ def parse_file(document, file, path)
239
+ text = File.read(file, encoding: "bom|utf-8")
240
+ case File.extname(file).downcase
241
+ when ".bib", ".bibtex"
242
+ Datalog::Citations::BibTeX.parse(text).map { |record| Datalog::Citations::Entry.from_bibtex(record) }
243
+ when ".json"
244
+ Array(JSON.parse(text)).map { |item| Datalog::Citations::Entry.from_csl(item) }
245
+ else
246
+ raise Jekyll::Errors::FatalException, "#{document.relative_path} names the bibliography #{path}, " \
247
+ "which is neither BibTeX (.bib) nor CSL-JSON (.json)"
129
248
  end
249
+ rescue Datalog::Citations::BibTeX::ParseError, JSON::ParserError => e
250
+ raise Jekyll::Errors::FatalException, "#{path}, the bibliography of #{document.relative_path}: #{e.message}"
130
251
  end
131
252
 
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
- {}
253
+ def site_setting(name)
254
+ settings = site.config["citations"]
255
+ settings.is_a?(Hash) ? settings[name] : nil
256
+ end
257
+
258
+ # ----------------------------------------------------------- numbering
259
+
260
+ # After conversion: resolve every placeholder, in the order the page shows them.
261
+ def number(document)
262
+ content = document.content.to_s
263
+ nocite = document.data["nocite"]
264
+ return unless content.include?('class="datalog-cite"') || nocite
265
+
266
+ state = load_state(document)
267
+ style = style(document)
268
+ words = words(document)
269
+ entries = listed_entries(document, state, content, nocite, style)
270
+ labels = Markup.labels(entries, style, words)
271
+ anchors = Markup.anchors(entries)
272
+ backlinks = Hash.new { |hash, key| hash[key] = [] }
273
+ document.content = content.gsub(MARKER) do
274
+ links = Regexp.last_match(1).split.map do |key|
275
+ ref = "cite-ref-#{anchors[key]}-#{backlinks[key].size + 1}"
276
+ backlinks[key] << ref
277
+ %(<a href="#cite-#{anchors[key]}" id="#{ref}">#{CGI.escapeHTML(labels[key])}</a>)
278
+ end
279
+ Markup.marker(links, Markup.locator_text(Regexp.last_match(2), words), style)
144
280
  end
281
+
282
+ state[:html] = Markup.list_html(entries, labels, anchors, backlinks, style, words)
283
+ publish(document, state,
284
+ "datalog_bibliography" => entries.map(&:key),
285
+ "datalog_cited_works" => entries.map { |entry| { "reference" => entry.highwire, "json_ld" => entry.json_ld } })
145
286
  end
146
287
 
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(". ")
288
+ # The cited entries in citation order, then the nocite ones; sorted by
289
+ # author and year in the author-year style.
290
+ def listed_entries(document, state, content, nocite, style)
291
+ cited = content.scan(MARKER).flat_map { |keys, _| keys.split }.uniq
292
+ check_keys(document, state, cited)
293
+ extra = [nocite].flatten.map(&:to_s) == ["all"] ? state[:entries].keys : Array(nocite).map(&:to_s)
294
+ check_keys(document, state, extra, "nocite")
295
+ entries = (cited + (extra - cited)).map { |key| state[:entries][key] }
296
+ style == "author-year" ? entries.sort_by(&:sort_key) : entries
159
297
  end
160
298
 
161
- def ensure_state(document)
162
- key = document.relative_path || document.path
163
- @state[key] = { key: key }.merge(@state[key] || {})
299
+ def check_keys(document, state, keys, tag = "cite")
300
+ missing = keys - state[:entries].keys
301
+ return if missing.empty?
302
+
303
+ verb = tag == "nocite" ? "lists in nocite" : "cites"
304
+ raise Jekyll::Errors::FatalException,
305
+ "#{document.relative_path} #{verb} #{missing.map { |key| "\"#{key}\"" }.join(', ')}, which " \
306
+ "#{missing.size == 1 ? 'is' : 'are'} not in its bibliography (#{state[:sources]})"
164
307
  end
165
308
 
166
- def resolve_state(document)
167
- key = document.data["datalog_citation_state"] || document.relative_path || document.path
168
- @state[key]
309
+ def bibliography_html(page)
310
+ state = @state[page["datalog_citation_state"]]
311
+ state && state[:html] ? state[:html] : ""
169
312
  end
170
313
 
171
- def state_from_context(context)
172
- page = context.registers[:page] || {}
173
- key = page["datalog_citation_state"]
174
- @state[key]
314
+ # --------------------------------------------------------------- excerpts
315
+
316
+ # Jekyll renders an excerpt apart from its post, and on a listing page
317
+ # before the post itself may be converted. Its citations read as they do
318
+ # in the post, linked to the post's bibliography.
319
+ def number_excerpt(excerpt, html)
320
+ return html unless html&.include?('class="datalog-cite"')
321
+
322
+ post = excerpt.doc
323
+ state = load_state(post)
324
+ style = style(post)
325
+ words = words(post)
326
+ cited = post.content.to_s.scan(MARKER).flat_map { |keys, _| keys.split }
327
+ cited = source_keys(post) if cited.empty?
328
+ entries = cited.uniq.filter_map { |key| state[:entries][key] }
329
+ entries = entries.sort_by(&:sort_key) if style == "author-year"
330
+ labels = Markup.labels(entries, style, words)
331
+ anchors = Markup.anchors(entries)
332
+ url = "#{post.site.baseurl}#{post.url}"
333
+ html.gsub(MARKER) do
334
+ links = Regexp.last_match(1).split.map do |key|
335
+ next CGI.escapeHTML(key) unless labels[key]
336
+
337
+ %(<a href="#{url}#cite-#{anchors[key]}">#{CGI.escapeHTML(labels[key])}</a>)
338
+ end
339
+ Markup.marker(links, Markup.locator_text(Regexp.last_match(2), words), style)
340
+ end
175
341
  end
176
342
 
177
- def missing_markup
178
- "<span class=\"datalog-citation datalog-citation--missing\">[citation]</span>"
343
+ def source_keys(post)
344
+ source = File.file?(post.path.to_s) ? File.read(post.path, encoding: "bom|utf-8") : ""
345
+ source.scan(SOURCE_TAG).flat_map { |(markup)| Citations.parse_markup(markup).first }
346
+ rescue Liquid::ArgumentError
347
+ []
348
+ end
349
+
350
+ # ----------------------------------------------------------- settings
351
+
352
+ def style(document)
353
+ value = (document.data["citation_style"] || site_setting("style") || "numeric").to_s
354
+ STYLES.include?(value) ? value : "numeric"
355
+ end
356
+
357
+ def words(document)
358
+ locale = Datalog::I18n.locale_code(site, document.data["lang"])
359
+ lookup = ->(key, fallback) { Datalog::I18n.lookup(site, locale, "citations.#{key}") || fallback }
360
+ {
361
+ and: lookup.call("and", "and"), et_al: lookup.call("et_al", "et al."),
362
+ no_date: lookup.call("no_date", "n.d."), back: lookup.call("back", "Back to citation {{n}}"),
363
+ locators: LOCATORS.to_h { |kind| [kind, lookup.call("locators.#{kind}", "#{kind}.")] }
364
+ }
365
+ end
366
+
367
+ # --------------------------------------------------------------- search
368
+
369
+ # The keys a page cites, for the search index, when the site has search.
370
+ def search_indexing(document)
371
+ return {} unless Datalog::PluginSystem.plugin("datalog-search")
372
+
373
+ keys = Array(document.data["datalog_bibliography"])
374
+ return {} if keys.empty?
375
+
376
+ { "citations" => { "count" => keys.size, "keys" => keys.sort } }
179
377
  end
180
378
  end
181
379
  end
182
380
  end
381
+
382
+ # The tags exist whether or not the plugin is on, so a page that cites on a
383
+ # site without it stops with a message saying what to enable, not with
384
+ # "Unknown tag 'cite'". The plugin registers them again when it loads.
385
+ Liquid::Template.register_tag("cite", Datalog::Plugins::Citations::CiteTag)
386
+ Liquid::Template.register_tag("datalog_cite", Datalog::Plugins::Citations::CiteTag)
387
+ Liquid::Template.register_tag("datalog_bibliography", Datalog::Plugins::Citations::BibliographyTag)
388
+
389
+ Jekyll::Hooks.register %i[pages documents], :post_convert do |document|
390
+ Datalog::PluginSystem.plugin("datalog-citations")&.number(document)
391
+ end
392
+
393
+ Jekyll::Excerpt.prepend(Module.new do
394
+ def output
395
+ plugin = Datalog::PluginSystem.plugin("datalog-citations")
396
+ plugin ? plugin.number_excerpt(self, super) : super
397
+ end
398
+ end)
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
4
+ require "yaml"
5
+
6
+ module Datalog
7
+ # A site's _config.yml as the CLI's commands read it, which has to be as
8
+ # Jekyll reads it: a date, a time or a YAML alias that builds fine must not
9
+ # stop `datalog update`, `check` or `critical-css`. Each command used to call
10
+ # the safe loader itself, and the updater's permitted no dates, so a site
11
+ # with `launched: 2024-01-01` in its configuration failed the update after
12
+ # the theme checkout had already moved.
13
+ module SiteConfig
14
+ PERMITTED_CLASSES = [Date, Time].freeze
15
+
16
+ module_function
17
+
18
+ # The settings in a configuration file, or {} when there is none.
19
+ # A file that is not valid YAML still raises Psych::SyntaxError.
20
+ def load(path)
21
+ return {} unless File.file?(path)
22
+
23
+ YAML.safe_load_file(path, permitted_classes: PERMITTED_CLASSES, aliases: true) || {}
24
+ end
25
+ end
26
+ end