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,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
require "digest"
|
|
5
|
+
|
|
6
|
+
module Datalog
|
|
7
|
+
module Security
|
|
8
|
+
class CspGenerator < Jekyll::Generator
|
|
9
|
+
priority :highest
|
|
10
|
+
|
|
11
|
+
DEFAULT_NONCE_BYTES = 16
|
|
12
|
+
|
|
13
|
+
def initialize(config = {})
|
|
14
|
+
super
|
|
15
|
+
@nonce_bytes = config.fetch("nonce_bytes", DEFAULT_NONCE_BYTES)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def generate(site)
|
|
19
|
+
site.data["csp"] ||= {}
|
|
20
|
+
site.data["csp"]["nonces"] ||= {}
|
|
21
|
+
site.data["csp"]["hashes"] ||= {}
|
|
22
|
+
|
|
23
|
+
assign_nonces(site, site.pages)
|
|
24
|
+
site.collections.each_value do |collection|
|
|
25
|
+
assign_nonces(site, collection.docs)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def assign_nonces(site, documents)
|
|
30
|
+
Array(documents).each do |doc|
|
|
31
|
+
self.class.assign_nonce(site, doc, @nonce_bytes)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def self.assign_nonce(site, document, nonce_bytes = DEFAULT_NONCE_BYTES)
|
|
36
|
+
return unless document.respond_to?(:data)
|
|
37
|
+
|
|
38
|
+
existing = document.data["csp_nonce"]
|
|
39
|
+
return existing if existing
|
|
40
|
+
|
|
41
|
+
nonce = SecureRandom.base64(nonce_bytes)
|
|
42
|
+
document.data["csp_nonce"] = nonce
|
|
43
|
+
document.data["csp_hashes"] ||= []
|
|
44
|
+
|
|
45
|
+
registry_site = site || (document.respond_to?(:site) ? document.site : nil)
|
|
46
|
+
if registry_site.respond_to?(:data)
|
|
47
|
+
registry_site.data["csp"] ||= {}
|
|
48
|
+
registry_site.data["csp"]["nonces"] ||= {}
|
|
49
|
+
key = document_key(document)
|
|
50
|
+
registry_site.data["csp"]["nonces"][key] = nonce if key
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
nonce
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def self.document_key(doc)
|
|
57
|
+
if doc.respond_to?(:relative_path) && doc.relative_path
|
|
58
|
+
doc.relative_path
|
|
59
|
+
elsif doc.respond_to?(:path)
|
|
60
|
+
doc.path
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def self.compute_hashes(document)
|
|
65
|
+
return unless document.respond_to?(:output)
|
|
66
|
+
|
|
67
|
+
output = document.output
|
|
68
|
+
return if output.nil? || output.empty?
|
|
69
|
+
|
|
70
|
+
site = document.respond_to?(:site) ? document.site : nil
|
|
71
|
+
nonce = assign_nonce(site, document)
|
|
72
|
+
|
|
73
|
+
# Case-insensitive: HTML tag names are, so <SCRIPT> in author content
|
|
74
|
+
# must be matched too. Without /i such a tag received no nonce and was
|
|
75
|
+
# then blocked by the policy, which fails closed but silently.
|
|
76
|
+
output = output.gsub(/<script(?![^>]*\bsrc=)(?![^>]*\bnonce=)([^>]*)>/i) do
|
|
77
|
+
attributes = Regexp.last_match(1)
|
|
78
|
+
"<script nonce=\"#{nonce}\"#{attributes}>"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
document.output = output
|
|
82
|
+
|
|
83
|
+
hashes = []
|
|
84
|
+
output.scan(%r{<script(?![^>]*\bsrc=)[^>]*>(.*?)</script>}mi) do |match|
|
|
85
|
+
content = match.first
|
|
86
|
+
next if content.nil? || content.empty?
|
|
87
|
+
|
|
88
|
+
hashes << Digest::SHA256.base64digest(content)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
hashes.uniq!
|
|
92
|
+
document.data["csp_hashes"] = hashes
|
|
93
|
+
|
|
94
|
+
return unless site.respond_to?(:data)
|
|
95
|
+
|
|
96
|
+
site.data["csp"] ||= {}
|
|
97
|
+
site.data["csp"]["hashes"] ||= {}
|
|
98
|
+
|
|
99
|
+
key = document_key(document)
|
|
100
|
+
|
|
101
|
+
site.data["csp"]["hashes"][key] = hashes if key
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
%i[pages documents].each do |target|
|
|
108
|
+
Jekyll::Hooks.register target, :pre_render do |document|
|
|
109
|
+
Datalog::Security::CspGenerator.assign_nonce(document.respond_to?(:site) ? document.site : nil, document)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
Jekyll::Hooks.register target, :post_render do |document|
|
|
113
|
+
Datalog::Security::CspGenerator.compute_hashes(document)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jekyll
|
|
4
|
+
class DatalogBibliographyTag < Liquid::Tag
|
|
5
|
+
def render(context)
|
|
6
|
+
page = context.registers[:page]
|
|
7
|
+
bib_data = page["datalog_bibliography"]
|
|
8
|
+
return "" unless bib_data
|
|
9
|
+
|
|
10
|
+
<<~HTML
|
|
11
|
+
<div class="datalog-bibliography-placeholder">
|
|
12
|
+
<p>Bibliography feature coming soon.</p>
|
|
13
|
+
</div>
|
|
14
|
+
HTML
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
Liquid::Template.register_tag("datalog_bibliography", Jekyll::DatalogBibliographyTag)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jekyll
|
|
4
|
+
class DatalogCommentsTag < Liquid::Tag
|
|
5
|
+
def render(context)
|
|
6
|
+
page = context.registers[:page]
|
|
7
|
+
return "" unless page["datalog_comments"]
|
|
8
|
+
|
|
9
|
+
<<~HTML
|
|
10
|
+
<div class="datalog-comments-placeholder">
|
|
11
|
+
<p>Comments feature coming soon.</p>
|
|
12
|
+
</div>
|
|
13
|
+
HTML
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
Liquid::Template.register_tag("datalog_comments", Jekyll::DatalogCommentsTag)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jekyll
|
|
4
|
+
class DatalogSlidesTag < Liquid::Tag
|
|
5
|
+
def render(context)
|
|
6
|
+
page = context.registers[:page]
|
|
7
|
+
slides_data = page["datalog_slides"]
|
|
8
|
+
return "" unless slides_data
|
|
9
|
+
|
|
10
|
+
# Render a simple placeholder or embed
|
|
11
|
+
<<~HTML
|
|
12
|
+
<div class="datalog-slides-placeholder">
|
|
13
|
+
<p>Slides feature coming soon. Configured: #{slides_data}</p>
|
|
14
|
+
</div>
|
|
15
|
+
HTML
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
Liquid::Template.register_tag("datalog_slides", Jekyll::DatalogSlidesTag)
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Minimal Mistakes front-matter compatibility.
|
|
4
|
+
#
|
|
5
|
+
# Sites migrating from the Minimal Mistakes theme carry front matter such as
|
|
6
|
+
#
|
|
7
|
+
# header:
|
|
8
|
+
# image: /assets/images/hero.jpg
|
|
9
|
+
# overlay_image: /assets/images/hero.jpg
|
|
10
|
+
# teaser: /assets/images/hero.jpg
|
|
11
|
+
# og_image: /assets/images/hero.jpg
|
|
12
|
+
# seo_title: Short title for search engines
|
|
13
|
+
# seo_description: Meta description
|
|
14
|
+
# classes: wide
|
|
15
|
+
#
|
|
16
|
+
# DataLog's layouts read `image`, `og_image`, `twitter_image`, `teaser`,
|
|
17
|
+
# `description` and an array of `classes`; the post hero is read from a
|
|
18
|
+
# `post_hero` hash. This hook fills those fields from the Minimal Mistakes
|
|
19
|
+
# ones when they are absent, so a migrated site keeps its hero images,
|
|
20
|
+
# teasers, titles and descriptions without every post being edited. Explicit
|
|
21
|
+
# DataLog fields always win: the mapping only fills gaps and never overwrites
|
|
22
|
+
# a value the author set.
|
|
23
|
+
#
|
|
24
|
+
# The hero is deliberately kept under its own key rather than `hero_image`,
|
|
25
|
+
# which DataLog's page and portfolio layouts already use for their own hero
|
|
26
|
+
# rendering.
|
|
27
|
+
#
|
|
28
|
+
# See docs/migrating-from-minimal-mistakes.md for the full field table.
|
|
29
|
+
module Datalog
|
|
30
|
+
module FrontMatterCompat
|
|
31
|
+
SOCIAL_IMAGE_KEYS = %w[og_image image overlay_image teaser].freeze
|
|
32
|
+
HERO_IMAGE_KEYS = %w[overlay_image image].freeze
|
|
33
|
+
TEASER_IMAGE_KEYS = %w[teaser image og_image overlay_image].freeze
|
|
34
|
+
|
|
35
|
+
module_function
|
|
36
|
+
|
|
37
|
+
# Normalises one document's front matter in place and returns it.
|
|
38
|
+
def normalize!(data)
|
|
39
|
+
return data unless data.is_a?(Hash)
|
|
40
|
+
|
|
41
|
+
header = data["header"].is_a?(Hash) ? data["header"] : {}
|
|
42
|
+
|
|
43
|
+
assign_missing(data, "og_image", header["og_image"])
|
|
44
|
+
assign_missing(data, "image", first_present(header, SOCIAL_IMAGE_KEYS))
|
|
45
|
+
assign_missing(data, "twitter_image", header["twitter_image"])
|
|
46
|
+
assign_missing(data, "teaser", first_present(header, TEASER_IMAGE_KEYS))
|
|
47
|
+
|
|
48
|
+
hero = first_present(header, HERO_IMAGE_KEYS)
|
|
49
|
+
data["post_hero"] = post_hero_from(header, hero) if hero && !data["post_hero"].is_a?(Hash)
|
|
50
|
+
|
|
51
|
+
assign_missing(data, "description", data["seo_description"])
|
|
52
|
+
data["classes"] = normalize_classes(data["classes"]) if data.key?("classes")
|
|
53
|
+
data
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def post_hero_from(header, image)
|
|
57
|
+
{
|
|
58
|
+
"image" => image,
|
|
59
|
+
"overlay" => overlay?(header),
|
|
60
|
+
"alt" => header["image_description"],
|
|
61
|
+
"caption" => header["caption"],
|
|
62
|
+
"overlay_filter" => header["overlay_filter"],
|
|
63
|
+
"overlay_color" => header["overlay_color"]
|
|
64
|
+
}.reject { |_key, value| !present?(value) && value != false }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Minimal Mistakes renders the title over the image when either an overlay
|
|
68
|
+
# image or an overlay colour is given; a plain `header.image` is a figure.
|
|
69
|
+
def overlay?(header)
|
|
70
|
+
present?(header["overlay_image"]) || present?(header["overlay_color"])
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def first_present(hash, keys)
|
|
74
|
+
keys.map { |key| hash[key] }.find { |value| present?(value) }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def assign_missing(data, key, value)
|
|
78
|
+
return unless present?(value)
|
|
79
|
+
return if present?(data[key])
|
|
80
|
+
|
|
81
|
+
data[key] = value
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# `classes: wide` (a string) and `classes: [wide]` (a list) both become a
|
|
85
|
+
# list, which is what the default layout joins into the body class.
|
|
86
|
+
def normalize_classes(value)
|
|
87
|
+
case value
|
|
88
|
+
when Array then value.flatten.compact.map(&:to_s).flat_map(&:split)
|
|
89
|
+
when String then value.split
|
|
90
|
+
else []
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def present?(value)
|
|
95
|
+
case value
|
|
96
|
+
when nil, false then false
|
|
97
|
+
when String then !value.strip.empty?
|
|
98
|
+
else true
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
Jekyll::Hooks.register :site, :post_read do |site|
|
|
105
|
+
site.collections.each_value do |collection|
|
|
106
|
+
collection.docs.each { |doc| Datalog::FrontMatterCompat.normalize!(doc.data) }
|
|
107
|
+
end
|
|
108
|
+
site.pages.each { |page| Datalog::FrontMatterCompat.normalize!(page.data) }
|
|
109
|
+
end
|
data/_plugins/i18n.rb
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Datalog
|
|
4
|
+
module I18n
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def locale(context)
|
|
8
|
+
page_lang = context["page"] && context["page"]["lang"]
|
|
9
|
+
site = context.registers[:site]
|
|
10
|
+
site_locale = site.config.dig("theme_options", "localization", "default_locale") || site.config["locale"] || "en"
|
|
11
|
+
lang = page_lang || site_locale
|
|
12
|
+
lang.to_s.split(/[-_]/).first
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def data_for(site)
|
|
16
|
+
site.data.fetch("i18n", {})
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def lookup(site, locale_code, key)
|
|
20
|
+
data = data_for(site)
|
|
21
|
+
locales = [locale_code, "en"].uniq
|
|
22
|
+
path = key.to_s.split(".")
|
|
23
|
+
|
|
24
|
+
locales.each do |code|
|
|
25
|
+
scope = data[code]
|
|
26
|
+
next unless scope
|
|
27
|
+
|
|
28
|
+
value = path.reduce(scope) do |memo, segment|
|
|
29
|
+
break unless memo.is_a?(Hash)
|
|
30
|
+
|
|
31
|
+
memo[segment] || memo[segment.to_sym]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
return value if value.is_a?(String)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
nil
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def interpolate(value, options)
|
|
41
|
+
return value unless options.is_a?(Hash) && !options.empty?
|
|
42
|
+
|
|
43
|
+
options.reduce(value) do |memo, (key, replacement)|
|
|
44
|
+
memo.gsub("{{#{key}}}", replacement.to_s)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def translate(context, key, options = {})
|
|
49
|
+
site = context.registers[:site]
|
|
50
|
+
locale_code = locale(context)
|
|
51
|
+
value = lookup(site, locale_code, key)
|
|
52
|
+
value ||= key
|
|
53
|
+
interpolate(value.to_s, stringify_keys(options))
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def stringify_keys(options)
|
|
57
|
+
return {} unless options
|
|
58
|
+
|
|
59
|
+
options.each_with_object({}) do |(key, value), memo|
|
|
60
|
+
memo[key.to_s] = value
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def localized_format(context, input, format_key = "long")
|
|
65
|
+
date = if input.respond_to?(:to_time)
|
|
66
|
+
input.to_time
|
|
67
|
+
else
|
|
68
|
+
Liquid::Utils.to_date(input)
|
|
69
|
+
end
|
|
70
|
+
return "" unless date
|
|
71
|
+
|
|
72
|
+
site = context.registers[:site]
|
|
73
|
+
locale_code = locale(context)
|
|
74
|
+
translations = data_for(site)
|
|
75
|
+
locale_data = translations[locale_code] || translations["en"] || {}
|
|
76
|
+
date_scope = locale_data["date"] || {}
|
|
77
|
+
formats = date_scope["formats"] || {}
|
|
78
|
+
format_string = formats[format_key] || formats["default"] || "%B %-d, %Y"
|
|
79
|
+
formatted = date.strftime(format_string)
|
|
80
|
+
|
|
81
|
+
months = date_scope["months"] || {}
|
|
82
|
+
short_months = date_scope["months_short"] || {}
|
|
83
|
+
weekdays = date_scope["weekdays"] || {}
|
|
84
|
+
short_weekdays = date_scope["weekdays_short"] || {}
|
|
85
|
+
|
|
86
|
+
if months && !months.empty?
|
|
87
|
+
formatted = replace_name(formatted, date.strftime("%B"),
|
|
88
|
+
months[date.month.to_s] || months[date.month])
|
|
89
|
+
end
|
|
90
|
+
if short_months && !short_months.empty?
|
|
91
|
+
formatted = replace_name(formatted, date.strftime("%b"),
|
|
92
|
+
short_months[date.month.to_s] || short_months[date.month])
|
|
93
|
+
end
|
|
94
|
+
if weekdays && !weekdays.empty?
|
|
95
|
+
formatted = replace_name(formatted, date.strftime("%A"),
|
|
96
|
+
weekdays[date.wday.to_s] || weekdays[date.wday])
|
|
97
|
+
end
|
|
98
|
+
if short_weekdays && !short_weekdays.empty?
|
|
99
|
+
formatted = replace_name(formatted, date.strftime("%a"),
|
|
100
|
+
short_weekdays[date.wday.to_s] || short_weekdays[date.wday])
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
formatted
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def replace_name(text, english, localized)
|
|
107
|
+
return text unless english && localized
|
|
108
|
+
|
|
109
|
+
text.gsub(english, localized)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
class TranslateTag < Liquid::Tag
|
|
115
|
+
# The key may be bare or quoted; a quoted key must not leak its closing
|
|
116
|
+
# quote into the first option name, or interpolation silently fails.
|
|
117
|
+
SYNTAX = /\A\s*(['"]?)(\w[\w.-]*)\1(.*)?\z/m
|
|
118
|
+
|
|
119
|
+
def initialize(tag_name, markup, tokens)
|
|
120
|
+
super
|
|
121
|
+
raise Liquid::SyntaxError, "Syntax Error in 't' - Valid syntax: t key [arg: value]" unless markup.strip =~ SYNTAX
|
|
122
|
+
|
|
123
|
+
@key = Regexp.last_match(2)
|
|
124
|
+
@markup = Regexp.last_match(3)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def render(context)
|
|
128
|
+
options = parse_options(@markup, context)
|
|
129
|
+
Datalog::I18n.translate(context, @key, options)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
private
|
|
133
|
+
|
|
134
|
+
def parse_options(markup, context)
|
|
135
|
+
return {} unless markup && !markup.strip.empty?
|
|
136
|
+
|
|
137
|
+
tokens = markup.strip.split(",").map(&:strip)
|
|
138
|
+
tokens.each_with_object({}) do |token, memo|
|
|
139
|
+
next if token.empty?
|
|
140
|
+
|
|
141
|
+
if token.include?(":")
|
|
142
|
+
key, value = token.split(":", 2)
|
|
143
|
+
memo[key.strip.to_sym] = context.evaluate(Liquid::Expression.parse(value.strip))
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
module TranslateFilter
|
|
150
|
+
def t(key, options = {})
|
|
151
|
+
Datalog::I18n.translate(@context, key, options)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def localize_date(input, format = "long")
|
|
155
|
+
Datalog::I18n.localized_format(@context, input, format)
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
Liquid::Template.register_tag("t", TranslateTag)
|
|
160
|
+
Liquid::Template.register_filter(TranslateFilter)
|