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,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)