jekyll-unirate 0.1.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 +23 -0
- data/LICENSE +21 -0
- data/README.md +158 -0
- data/jekyll-unirate.gemspec +51 -0
- data/lib/jekyll/unirate/client.rb +90 -0
- data/lib/jekyll/unirate/config.rb +52 -0
- data/lib/jekyll/unirate/filters.rb +64 -0
- data/lib/jekyll/unirate/formatter.rb +54 -0
- data/lib/jekyll/unirate/generator.rb +64 -0
- data/lib/jekyll/unirate/snapshot.rb +68 -0
- data/lib/jekyll/unirate/tags.rb +84 -0
- data/lib/jekyll/unirate/version.rb +7 -0
- data/lib/jekyll/unirate.rb +27 -0
- data/lib/jekyll-unirate.rb +5 -0
- metadata +142 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 545b7fd8b44a778643ca5ac64a96481a5b36f69db89711d0aa5e78516aa3df56
|
|
4
|
+
data.tar.gz: 65c8702502582332fddd832e698c5d368b365b237b9aa4ad5be3cfd6ec3e6eed
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 49fc0bbf2d4de414e2ef9c615be0849e81efdd53c9294760e0df16cc124722317b9cddbe5bd304ca5b0e798809f641d19673fbd56dc5cc57010df2403494e5fe
|
|
7
|
+
data.tar.gz: 6a7bb07ee5cda9a8f0ccac9d42221d38f7c8f37c0b4bf868d98a0c1d4e1fc6c0fd7cc1ef9cc7ad981af2178501cfea9c24be27f1d3a1bc52b4bd09ea3d4ccbd7
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. The format is based
|
|
4
|
+
on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project
|
|
5
|
+
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [0.1.0] - 2026-06-19
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Initial release.
|
|
12
|
+
- Build-time `Jekyll::Generator` that fetches one single-base exchange-rate
|
|
13
|
+
snapshot from the UniRate API and exposes it to the build.
|
|
14
|
+
- Liquid filters: `unirate_rate`, `unirate_convert`, `unirate_money`,
|
|
15
|
+
`unirate_price`.
|
|
16
|
+
- Liquid tags: `{% unirate_rate %}`, `{% unirate_convert %}`,
|
|
17
|
+
`{% unirate_price %}`.
|
|
18
|
+
- `site.data["unirate"]` rate map for direct template iteration.
|
|
19
|
+
- Cross-rates derived on demand from the single-base snapshot.
|
|
20
|
+
- Graceful degradation: a failed fetch logs a warning and never breaks the
|
|
21
|
+
build; helpers fall back to unconverted amounts.
|
|
22
|
+
|
|
23
|
+
[0.1.0]: https://github.com/UniRate-API/jekyll-unirate/releases/tag/v0.1.0
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Unirate Team
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# jekyll-unirate
|
|
2
|
+
|
|
3
|
+
[](https://github.com/UniRate-API/jekyll-unirate/actions/workflows/ci.yml)
|
|
4
|
+
[](https://rubygems.org/gems/jekyll-unirate)
|
|
5
|
+
|
|
6
|
+
Currency conversion and live exchange rates for [Jekyll](https://jekyllrb.com),
|
|
7
|
+
powered by the [UniRate API](https://unirateapi.com).
|
|
8
|
+
|
|
9
|
+
`jekyll-unirate` fetches **one** exchange-rate snapshot at build time and
|
|
10
|
+
exposes Liquid filters and tags to convert amounts, format prices, and look up
|
|
11
|
+
rates anywhere in your site. Every cross-rate is derived on demand from a single
|
|
12
|
+
base snapshot, so one HTTP request covers all currency pairs — and if the API is
|
|
13
|
+
briefly unreachable, **your build still succeeds** with amounts rendered
|
|
14
|
+
unconverted rather than failing.
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
Add the gem to your site's `Gemfile`, in the `:jekyll_plugins` group:
|
|
19
|
+
|
|
20
|
+
```ruby
|
|
21
|
+
group :jekyll_plugins do
|
|
22
|
+
gem "jekyll-unirate"
|
|
23
|
+
end
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Then `bundle install`. (Or add `jekyll-unirate` to the `plugins:` list in
|
|
27
|
+
`_config.yml` if you manage plugins there.)
|
|
28
|
+
|
|
29
|
+
## Configuration
|
|
30
|
+
|
|
31
|
+
Configure the plugin under a `unirate:` key in `_config.yml`:
|
|
32
|
+
|
|
33
|
+
```yaml
|
|
34
|
+
unirate:
|
|
35
|
+
base: USD # currency the rate snapshot is fetched against
|
|
36
|
+
default_currency: USD # display currency for formatting helpers
|
|
37
|
+
base_url: https://api.unirateapi.com
|
|
38
|
+
timeout: 30
|
|
39
|
+
# api_key: ... # prefer the UNIRATE_API_KEY env var (see below)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### API key
|
|
43
|
+
|
|
44
|
+
Get a free key at [unirateapi.com](https://unirateapi.com). Provide it via the
|
|
45
|
+
`UNIRATE_API_KEY` environment variable so it never gets committed to your site
|
|
46
|
+
repository:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
UNIRATE_API_KEY=your-key bundle exec jekyll build
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
The env var takes precedence; `unirate.api_key` in `_config.yml` is a fallback.
|
|
53
|
+
If no key is configured, the build still runs — helpers just render amounts
|
|
54
|
+
unconverted and a warning is logged.
|
|
55
|
+
|
|
56
|
+
## Usage
|
|
57
|
+
|
|
58
|
+
### Filters
|
|
59
|
+
|
|
60
|
+
```liquid
|
|
61
|
+
{{ "USD" | unirate_rate: "EUR" }} {%- comment %} 0.92 {% endcomment -%}
|
|
62
|
+
{{ 100 | unirate_convert: "USD", "EUR" }} {%- comment %} 92.0 {% endcomment -%}
|
|
63
|
+
{{ 92.5 | unirate_money: "EUR" }} {%- comment %} €92.50 {% endcomment -%}
|
|
64
|
+
{{ 100 | unirate_price: "USD", "EUR" }} {%- comment %} €92.00 {% endcomment -%}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
| Filter | Returns |
|
|
68
|
+
|---|---|
|
|
69
|
+
| `unirate_rate: from, to` | Numeric exchange rate (empty if unknown) |
|
|
70
|
+
| `unirate_convert: from, to` | Converted number (falls back to the input amount) |
|
|
71
|
+
| `unirate_money: currency` | A bare amount formatted with the currency symbol |
|
|
72
|
+
| `unirate_price: from, to` | Converted **and** formatted in the target currency |
|
|
73
|
+
|
|
74
|
+
Front-matter variables work as arguments too:
|
|
75
|
+
|
|
76
|
+
```liquid
|
|
77
|
+
{{ page.price | unirate_price: page.base_currency, site.currency }}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Tags
|
|
81
|
+
|
|
82
|
+
```liquid
|
|
83
|
+
{% unirate_rate USD EUR %} {%- comment %} 0.92 {% endcomment -%}
|
|
84
|
+
{% unirate_convert 100 USD EUR %} {%- comment %} 92.0 {% endcomment -%}
|
|
85
|
+
{% unirate_price 100 USD EUR %} {%- comment %} €92.00 {% endcomment -%}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Tag arguments may be literals (quoted or bare) or Liquid variables resolved
|
|
89
|
+
against the current context:
|
|
90
|
+
|
|
91
|
+
```liquid
|
|
92
|
+
{% unirate_price item.price USD page.currency %}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Raw rate data
|
|
96
|
+
|
|
97
|
+
The full snapshot is also published to `site.data["unirate"]` so you can iterate
|
|
98
|
+
it directly:
|
|
99
|
+
|
|
100
|
+
```liquid
|
|
101
|
+
Rates as of build (base {{ site.data.unirate.base }}):
|
|
102
|
+
{% for pair in site.data.unirate.rates %}
|
|
103
|
+
1 {{ site.data.unirate.base }} = {{ pair[1] }} {{ pair[0] }}
|
|
104
|
+
{% endfor %}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
`site.data.unirate.updated` is `true` when rates loaded successfully and `false`
|
|
108
|
+
when the fetch was skipped or failed.
|
|
109
|
+
|
|
110
|
+
## Formatting
|
|
111
|
+
|
|
112
|
+
`unirate_money` / `unirate_price` use a lightweight built-in formatter (symbol
|
|
113
|
+
table + `#,##0.00` grouping, with zero-decimal currencies like JPY handled).
|
|
114
|
+
It is intentionally dependency-free and not a substitute for full locale-aware
|
|
115
|
+
i18n — for anything richer, use `unirate_convert` to get the number and format
|
|
116
|
+
it yourself.
|
|
117
|
+
|
|
118
|
+
## Graceful degradation
|
|
119
|
+
|
|
120
|
+
This plugin is built to **never break your build**:
|
|
121
|
+
|
|
122
|
+
- No API key → fetch skipped, amounts render unconverted, warning logged.
|
|
123
|
+
- API error / network failure → warning logged, build continues, helpers fall
|
|
124
|
+
back to the input amount.
|
|
125
|
+
|
|
126
|
+
Mapped API responses: `401` (bad/missing key), `403` (Pro-gated endpoint),
|
|
127
|
+
`404` (unknown currency), `429` (rate limit), plus network and malformed-response
|
|
128
|
+
errors.
|
|
129
|
+
|
|
130
|
+
## Development
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
bundle install
|
|
134
|
+
bundle exec rspec # WebMock-based mock tests
|
|
135
|
+
bundle exec rubocop
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
<!-- unirate-ecosystem-footer:start -->
|
|
139
|
+
## Other UniRate clients
|
|
140
|
+
|
|
141
|
+
UniRate ships official client libraries and framework integrations across the
|
|
142
|
+
ecosystem. The repos below are all maintained under the
|
|
143
|
+
[UniRate-API](https://github.com/UniRate-API) org.
|
|
144
|
+
|
|
145
|
+
- **Languages:** [Python](https://github.com/UniRate-API/unirate-api-python) · [Node.js / TypeScript](https://github.com/UniRate-API/unirate-api-nodejs) · [Go](https://github.com/UniRate-API/unirate-api-go) · [Rust](https://github.com/UniRate-API/unirate-api-rust) · [Java](https://github.com/UniRate-API/unirate-api-java) · [Ruby](https://github.com/UniRate-API/unirate-api-ruby) · [PHP](https://github.com/UniRate-API/unirate-api-php) · [.NET](https://github.com/UniRate-API/unirate-api-dotnet) · [Swift](https://github.com/UniRate-API/unirate-api-swift)
|
|
146
|
+
- **Web frameworks:** [NestJS](https://github.com/UniRate-API/nestjs-unirate) · [Next.js](https://github.com/UniRate-API/next-unirate) · [Nuxt](https://github.com/UniRate-API/nuxt-unirate) · [SvelteKit](https://github.com/UniRate-API/sveltekit-unirate) · [Django / Wagtail](https://github.com/UniRate-API/wagtail-unirate) · [FastAPI](https://github.com/UniRate-API/fastapi-unirate) · [Flask](https://github.com/UniRate-API/flask-unirate) · [React](https://github.com/UniRate-API/react-unirate) · [Vue](https://github.com/UniRate-API/vue-unirate) · [tRPC](https://github.com/UniRate-API/trpc-unirate)
|
|
147
|
+
- **Static-site generators:** [Astro](https://github.com/UniRate-API/astro-unirate) · [Eleventy](https://github.com/UniRate-API/eleventy-unirate) · [Hugo](https://github.com/UniRate-API/hugo-unirate) · [Jekyll](https://github.com/UniRate-API/jekyll-unirate)
|
|
148
|
+
- **Data / orchestration:** [Airflow](https://github.com/UniRate-API/airflow-provider-unirate) · [dbt](https://github.com/UniRate-API/dbt-unirate) · [LangChain](https://github.com/UniRate-API/langchain-unirate)
|
|
149
|
+
- **Workflow / no-code:** [n8n](https://github.com/UniRate-API/n8n-nodes-unirate) · [Google Sheets](https://github.com/UniRate-API/unirate-sheets) · [MCP server](https://github.com/UniRate-API/unirate-mcp)
|
|
150
|
+
- **Editors / tools:** [VS Code](https://github.com/UniRate-API/vscode-unirate) · [Obsidian](https://github.com/UniRate-API/obsidian-currency)
|
|
151
|
+
- **Specialty bridges:** [NodaMoney (.NET)](https://github.com/UniRate-API/UniRateApi.NodaMoney) · [money gem (Ruby)](https://github.com/UniRate-API/money-unirate-api) · [Laravel Money](https://github.com/UniRate-API/laravel-money-unirate)
|
|
152
|
+
|
|
153
|
+
Get a free API key at [unirateapi.com](https://unirateapi.com).
|
|
154
|
+
<!-- unirate-ecosystem-footer:end -->
|
|
155
|
+
|
|
156
|
+
## License
|
|
157
|
+
|
|
158
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
lib = File.expand_path("lib", __dir__)
|
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
5
|
+
require "jekyll/unirate/version"
|
|
6
|
+
|
|
7
|
+
Gem::Specification.new do |spec|
|
|
8
|
+
spec.name = "jekyll-unirate"
|
|
9
|
+
spec.version = Jekyll::Unirate::VERSION
|
|
10
|
+
spec.authors = ["Unirate Team"]
|
|
11
|
+
spec.email = ["admin@unirateapi.com"]
|
|
12
|
+
|
|
13
|
+
spec.summary = "UniRate currency conversion for Jekyll — Liquid tags & filters."
|
|
14
|
+
spec.description = "A Jekyll plugin backed by the UniRate API " \
|
|
15
|
+
"(https://unirateapi.com). Fetches one exchange-rate " \
|
|
16
|
+
"snapshot at build time and exposes Liquid filters and " \
|
|
17
|
+
"tags to convert, format, and look up currency rates, " \
|
|
18
|
+
"with cross-rates derived on demand. Fails gracefully so " \
|
|
19
|
+
"an API blip never breaks the build."
|
|
20
|
+
spec.homepage = "https://github.com/UniRate-API/jekyll-unirate"
|
|
21
|
+
spec.license = "MIT"
|
|
22
|
+
|
|
23
|
+
spec.required_ruby_version = ">= 3.0"
|
|
24
|
+
|
|
25
|
+
spec.metadata = {
|
|
26
|
+
"homepage_uri" => spec.homepage,
|
|
27
|
+
"source_code_uri" => spec.homepage,
|
|
28
|
+
"bug_tracker_uri" => "#{spec.homepage}/issues",
|
|
29
|
+
"changelog_uri" => "#{spec.homepage}/blob/main/CHANGELOG.md",
|
|
30
|
+
"documentation_uri" => "https://unirateapi.com",
|
|
31
|
+
"rubygems_mfa_required" => "true"
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
spec.files = Dir[
|
|
35
|
+
"lib/**/*.rb",
|
|
36
|
+
"README.md",
|
|
37
|
+
"CHANGELOG.md",
|
|
38
|
+
"LICENSE",
|
|
39
|
+
"jekyll-unirate.gemspec"
|
|
40
|
+
]
|
|
41
|
+
spec.require_paths = ["lib"]
|
|
42
|
+
|
|
43
|
+
# Jekyll is the host framework, always present in a Jekyll site. Net/HTTP and
|
|
44
|
+
# JSON come from the standard library, so there are no other runtime deps.
|
|
45
|
+
spec.add_dependency "jekyll", ">= 3.7", "< 5.0"
|
|
46
|
+
|
|
47
|
+
spec.add_development_dependency "rake", "~> 13.0"
|
|
48
|
+
spec.add_development_dependency "rspec", "~> 3.12"
|
|
49
|
+
spec.add_development_dependency "rubocop", "~> 1.60"
|
|
50
|
+
spec.add_development_dependency "webmock", "~> 3.19"
|
|
51
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bigdecimal"
|
|
4
|
+
require "json"
|
|
5
|
+
require "net/http"
|
|
6
|
+
require "uri"
|
|
7
|
+
|
|
8
|
+
require_relative "version"
|
|
9
|
+
|
|
10
|
+
module Jekyll
|
|
11
|
+
module Unirate
|
|
12
|
+
# Raised for any UniRate-specific failure: a network problem, an auth
|
|
13
|
+
# error, a Pro-gated endpoint, or an unexpected response shape. The
|
|
14
|
+
# generator rescues this and logs a warning so a transient API blip never
|
|
15
|
+
# breaks the site build.
|
|
16
|
+
class Error < StandardError; end
|
|
17
|
+
|
|
18
|
+
# Minimal stdlib-only HTTP client for the UniRate API
|
|
19
|
+
# (https://unirateapi.com). Pulls a single-base rate snapshot from
|
|
20
|
+
# `GET /api/rates`; cross-rates are derived from it by {Snapshot}.
|
|
21
|
+
#
|
|
22
|
+
# Zero runtime dependencies beyond Ruby's standard library — `net/http`
|
|
23
|
+
# and `json`. No third-party HTTP gem, so nothing extra to audit.
|
|
24
|
+
class Client
|
|
25
|
+
DEFAULT_BASE_URL = "https://api.unirateapi.com"
|
|
26
|
+
DEFAULT_TIMEOUT = 30
|
|
27
|
+
|
|
28
|
+
def initialize(api_key:, base_url: DEFAULT_BASE_URL, timeout: DEFAULT_TIMEOUT)
|
|
29
|
+
raise Error, "UniRate API key is required" if api_key.nil? || api_key.to_s.empty?
|
|
30
|
+
|
|
31
|
+
@api_key = api_key
|
|
32
|
+
@base_url = base_url
|
|
33
|
+
@timeout = timeout
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Fetch a single-base snapshot. Returns a {Snapshot} whose `rates` map is
|
|
37
|
+
# { "EUR" => BigDecimal, ... } expressed against +base+.
|
|
38
|
+
def fetch_snapshot(base)
|
|
39
|
+
base = base.to_s.upcase
|
|
40
|
+
body = http_get("/api/rates", "from" => base)
|
|
41
|
+
rates = body["rates"]
|
|
42
|
+
raise Error, 'Unexpected UniRate response: missing "rates"' unless rates.is_a?(Hash)
|
|
43
|
+
|
|
44
|
+
parsed = rates.each_with_object({}) do |(code, value), out|
|
|
45
|
+
out[code.to_s.upcase] = BigDecimal(value.to_s)
|
|
46
|
+
end
|
|
47
|
+
Snapshot.new(base: base, rates: parsed)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def http_get(path, params)
|
|
53
|
+
uri = URI.parse(@base_url)
|
|
54
|
+
uri.path = path
|
|
55
|
+
uri.query = URI.encode_www_form(params.merge("api_key" => @api_key))
|
|
56
|
+
|
|
57
|
+
req = Net::HTTP::Get.new(uri)
|
|
58
|
+
req["Accept"] = "application/json"
|
|
59
|
+
req["User-Agent"] = "jekyll-unirate/#{VERSION}"
|
|
60
|
+
|
|
61
|
+
parse(perform(uri, req))
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def perform(uri, req)
|
|
65
|
+
Net::HTTP.start(uri.hostname, uri.port,
|
|
66
|
+
use_ssl: uri.scheme == "https",
|
|
67
|
+
open_timeout: @timeout,
|
|
68
|
+
read_timeout: @timeout) { |http| http.request(req) }
|
|
69
|
+
rescue StandardError => e
|
|
70
|
+
raise Error, "Network error talking to UniRate: #{e.message}"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def parse(response)
|
|
74
|
+
status = response.code.to_i
|
|
75
|
+
case status
|
|
76
|
+
when 200..299
|
|
77
|
+
JSON.parse(response.body.to_s)
|
|
78
|
+
when 401 then raise Error, "Missing or invalid UniRate API key (HTTP 401)"
|
|
79
|
+
when 403 then raise Error, "This UniRate endpoint requires a Pro subscription (HTTP 403)"
|
|
80
|
+
when 404 then raise Error, "Currency not found or no data available (HTTP 404)"
|
|
81
|
+
when 429 then raise Error, "UniRate rate limit exceeded (HTTP 429)"
|
|
82
|
+
else
|
|
83
|
+
raise Error, "UniRate API error (HTTP #{status}): #{response.body}"
|
|
84
|
+
end
|
|
85
|
+
rescue JSON::ParserError => e
|
|
86
|
+
raise Error, "Failed to parse UniRate response: #{e.message}"
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jekyll
|
|
4
|
+
module Unirate
|
|
5
|
+
# Resolves plugin configuration from `_config.yml` (the `unirate:` key)
|
|
6
|
+
# layered over environment variables and sensible defaults.
|
|
7
|
+
#
|
|
8
|
+
# unirate:
|
|
9
|
+
# base: USD # currency the snapshot is fetched against
|
|
10
|
+
# default_currency: USD # display currency for one-arg helpers
|
|
11
|
+
# base_url: https://api.unirateapi.com
|
|
12
|
+
# timeout: 30
|
|
13
|
+
# # api_key: prefer the UNIRATE_API_KEY env var over committing a key
|
|
14
|
+
#
|
|
15
|
+
# The API key resolves from (in order): the UNIRATE_API_KEY env var, then
|
|
16
|
+
# `unirate.api_key` in _config.yml. Keeping it in the environment avoids
|
|
17
|
+
# checking a secret into the site repo.
|
|
18
|
+
class Config
|
|
19
|
+
DEFAULTS = {
|
|
20
|
+
"base" => "USD",
|
|
21
|
+
"default_currency" => "USD",
|
|
22
|
+
"base_url" => Client::DEFAULT_BASE_URL,
|
|
23
|
+
"timeout" => Client::DEFAULT_TIMEOUT
|
|
24
|
+
}.freeze
|
|
25
|
+
|
|
26
|
+
def initialize(site_config = {})
|
|
27
|
+
raw = site_config && site_config["unirate"]
|
|
28
|
+
@settings = DEFAULTS.merge(stringify(raw))
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def api_key
|
|
32
|
+
env = ENV.fetch("UNIRATE_API_KEY", nil)
|
|
33
|
+
return env unless env.nil? || env.empty?
|
|
34
|
+
|
|
35
|
+
@settings["api_key"]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def base = @settings["base"].to_s.upcase
|
|
39
|
+
def default_currency = @settings["default_currency"].to_s.upcase
|
|
40
|
+
def base_url = @settings["base_url"]
|
|
41
|
+
def timeout = @settings["timeout"]
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def stringify(hash)
|
|
46
|
+
return {} unless hash.is_a?(Hash)
|
|
47
|
+
|
|
48
|
+
hash.each_with_object({}) { |(k, v), out| out[k.to_s] = v }
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "liquid"
|
|
4
|
+
|
|
5
|
+
require_relative "snapshot"
|
|
6
|
+
require_relative "formatter"
|
|
7
|
+
|
|
8
|
+
module Jekyll
|
|
9
|
+
module Unirate
|
|
10
|
+
# Liquid filters for use in any Jekyll template. All are prefixed
|
|
11
|
+
# `unirate_` to avoid clashing with site- or theme-defined filters.
|
|
12
|
+
#
|
|
13
|
+
# {{ 100 | unirate_convert: "USD", "EUR" }} => 92.0
|
|
14
|
+
# {{ "USD" | unirate_rate: "EUR" }} => 0.92
|
|
15
|
+
# {{ 92.5 | unirate_money: "EUR" }} => "€92.50"
|
|
16
|
+
# {{ 100 | unirate_price: "USD", "EUR" }} => "€92.00"
|
|
17
|
+
#
|
|
18
|
+
# Every filter degrades gracefully when no snapshot is available or a pair
|
|
19
|
+
# is missing: `unirate_convert`/`unirate_price` fall back to the input
|
|
20
|
+
# amount (so a price still renders), `unirate_rate` returns nil. This keeps
|
|
21
|
+
# a transient API problem from breaking the rendered page.
|
|
22
|
+
module Filters
|
|
23
|
+
# Numeric exchange rate from +from+ to +to+ (Float), or nil if unknown.
|
|
24
|
+
def unirate_rate(from, to)
|
|
25
|
+
snapshot = Snapshot.current
|
|
26
|
+
return nil if snapshot.nil?
|
|
27
|
+
|
|
28
|
+
rate = snapshot.rate(from, to)
|
|
29
|
+
rate&.to_f
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Convert +amount+ from +from+ to +to+ (Float). Falls back to the input
|
|
33
|
+
# amount unchanged when the rate is unavailable.
|
|
34
|
+
def unirate_convert(amount, from, to)
|
|
35
|
+
snapshot = Snapshot.current
|
|
36
|
+
return to_number(amount) if snapshot.nil?
|
|
37
|
+
|
|
38
|
+
converted = snapshot.convert(amount, from, to)
|
|
39
|
+
converted ? converted.to_f : to_number(amount)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Format a bare numeric +amount+ in +currency+ (e.g. "€92.50").
|
|
43
|
+
def unirate_money(amount, currency)
|
|
44
|
+
Formatter.format(amount, currency)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Convert +amount+ from +from+ to +to+ and format it in +to+. Falls back
|
|
48
|
+
# to formatting the input amount in +to+ when no rate is available.
|
|
49
|
+
def unirate_price(amount, from, to)
|
|
50
|
+
Formatter.format(unirate_convert(amount, from, to), to)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def to_number(amount)
|
|
56
|
+
Float(amount)
|
|
57
|
+
rescue ArgumentError, TypeError
|
|
58
|
+
amount
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
Liquid::Template.register_filter(Jekyll::Unirate::Filters)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bigdecimal"
|
|
4
|
+
|
|
5
|
+
module Jekyll
|
|
6
|
+
module Unirate
|
|
7
|
+
# Lightweight currency formatter. Ruby's stdlib has no locale-aware money
|
|
8
|
+
# formatting and we deliberately avoid a heavy dependency (e.g. the `money`
|
|
9
|
+
# gem) for a static-site helper, so this is a pragmatic symbol table plus
|
|
10
|
+
# fixed grouping: `#,##0.00` with the symbol prefixed (or the ISO code when
|
|
11
|
+
# the symbol is unknown). Good enough for prices on a static page; not a
|
|
12
|
+
# substitute for full i18n.
|
|
13
|
+
module Formatter
|
|
14
|
+
SYMBOLS = {
|
|
15
|
+
"USD" => "$", "EUR" => "€", "GBP" => "£", "JPY" => "¥",
|
|
16
|
+
"CNY" => "¥", "AUD" => "A$", "CAD" => "C$", "CHF" => "CHF ",
|
|
17
|
+
"HKD" => "HK$", "NZD" => "NZ$", "SEK" => "kr ", "KRW" => "₩",
|
|
18
|
+
"SGD" => "S$", "NOK" => "kr ", "MXN" => "MX$", "INR" => "₹",
|
|
19
|
+
"RUB" => "₽", "ZAR" => "R", "TRY" => "₺", "BRL" => "R$",
|
|
20
|
+
"TWD" => "NT$", "DKK" => "kr ", "PLN" => "zł ",
|
|
21
|
+
"THB" => "฿", "IDR" => "Rp", "HUF" => "Ft ", "CZK" => "Kč ",
|
|
22
|
+
"ILS" => "₪", "PHP" => "₱", "AED" => "د.إ ",
|
|
23
|
+
"SAR" => "ر.س ", "MYR" => "RM", "RON" => "lei ",
|
|
24
|
+
"NGN" => "₦", "BTC" => "₿"
|
|
25
|
+
}.freeze
|
|
26
|
+
|
|
27
|
+
# Currencies conventionally shown without decimal places.
|
|
28
|
+
ZERO_DECIMAL = %w[JPY KRW HUF IDR CLP VND ISK].freeze
|
|
29
|
+
|
|
30
|
+
module_function
|
|
31
|
+
|
|
32
|
+
# Format +amount+ in +currency+. Returns a String such as "€1,234.50".
|
|
33
|
+
def format(amount, currency, decimals: nil)
|
|
34
|
+
currency = currency.to_s.upcase
|
|
35
|
+
decimals ||= ZERO_DECIMAL.include?(currency) ? 0 : 2
|
|
36
|
+
rounded = BigDecimal(amount.to_s).round(decimals)
|
|
37
|
+
sign = rounded.negative? ? "-" : ""
|
|
38
|
+
number = group(rounded.abs, decimals)
|
|
39
|
+
symbol = SYMBOLS[currency]
|
|
40
|
+
# Sign goes outside the symbol ("-$1,234.50"), the conventional form.
|
|
41
|
+
symbol ? "#{sign}#{symbol}#{number}" : "#{sign}#{number} #{currency}"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Group a non-negative decimal with thousands separators and fixed
|
|
45
|
+
# decimals. Calls Kernel.format explicitly — the bareword `format` would
|
|
46
|
+
# resolve to this module's own currency-formatting method.
|
|
47
|
+
def group(value, decimals)
|
|
48
|
+
whole, frac = Kernel.format("%.#{decimals}f", value).split(".")
|
|
49
|
+
whole = whole.reverse.gsub(/(\d{3})(?=\d)/, '\1,').reverse
|
|
50
|
+
frac ? "#{whole}.#{frac}" : whole
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "jekyll"
|
|
4
|
+
|
|
5
|
+
require_relative "client"
|
|
6
|
+
require_relative "snapshot"
|
|
7
|
+
require_relative "config"
|
|
8
|
+
|
|
9
|
+
module Jekyll
|
|
10
|
+
module Unirate
|
|
11
|
+
# Build-time generator that fetches ONE single-base UniRate snapshot and
|
|
12
|
+
# makes it available to the rest of the build. Runs early (high priority)
|
|
13
|
+
# so the snapshot is ready before any page renders.
|
|
14
|
+
#
|
|
15
|
+
# It does two things:
|
|
16
|
+
# 1. sets {Snapshot.current}, which the Liquid filters and tags read; and
|
|
17
|
+
# 2. populates `site.data["unirate"]` so templates can iterate the raw
|
|
18
|
+
# rate map directly (`{% for pair in site.data.unirate.rates %}`).
|
|
19
|
+
#
|
|
20
|
+
# A failed fetch is logged as a warning and never raised — the build
|
|
21
|
+
# continues and helpers degrade to unconverted amounts. This is the same
|
|
22
|
+
# "never break the build" contract the Hugo/Eleventy/Astro integrations use.
|
|
23
|
+
class Generator < Jekyll::Generator
|
|
24
|
+
priority :high
|
|
25
|
+
|
|
26
|
+
def generate(site)
|
|
27
|
+
config = Config.new(site.config)
|
|
28
|
+
Jekyll::Unirate.default_currency = config.default_currency
|
|
29
|
+
|
|
30
|
+
key = config.api_key
|
|
31
|
+
if key.nil? || key.to_s.empty?
|
|
32
|
+
Jekyll.logger.warn("UniRate:",
|
|
33
|
+
"no API key (set UNIRATE_API_KEY or unirate.api_key in _config.yml); " \
|
|
34
|
+
"skipping rate fetch")
|
|
35
|
+
return store_empty(site, config.base)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
load_rates(site, config)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def load_rates(site, config)
|
|
44
|
+
client = Client.new(api_key: config.api_key, base_url: config.base_url, timeout: config.timeout)
|
|
45
|
+
snapshot = client.fetch_snapshot(config.base)
|
|
46
|
+
Snapshot.current = snapshot
|
|
47
|
+
site.data["unirate"] = {
|
|
48
|
+
"base" => snapshot.base,
|
|
49
|
+
"rates" => snapshot.rates.transform_values(&:to_f),
|
|
50
|
+
"updated" => true
|
|
51
|
+
}
|
|
52
|
+
Jekyll.logger.info("UniRate:", "loaded #{snapshot.rates.size} rates (base #{snapshot.base})")
|
|
53
|
+
rescue Error => e
|
|
54
|
+
Jekyll.logger.warn("UniRate:",
|
|
55
|
+
"rate fetch failed (#{e.message}); pages will render with unconverted amounts")
|
|
56
|
+
store_empty(site, config.base)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def store_empty(site, base)
|
|
60
|
+
site.data["unirate"] = { "base" => base, "rates" => {}, "updated" => false }
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bigdecimal"
|
|
4
|
+
|
|
5
|
+
module Jekyll
|
|
6
|
+
module Unirate
|
|
7
|
+
# An immutable single-base rate snapshot. One HTTP call to UniRate fetches
|
|
8
|
+
# base->code rates; every cross-rate is derived from this snapshot on
|
|
9
|
+
# demand (`rate(F,T) = base->T / base->F`), so the whole build shares one
|
|
10
|
+
# frozen view of the market and no per-pair caching can go stale.
|
|
11
|
+
class Snapshot
|
|
12
|
+
attr_reader :base, :rates
|
|
13
|
+
|
|
14
|
+
# Process-wide current snapshot, set by {Generator} at build time and
|
|
15
|
+
# read by the Liquid filters and tags. nil until the generator runs (or
|
|
16
|
+
# if the fetch failed) — callers must degrade gracefully when it is.
|
|
17
|
+
class << self
|
|
18
|
+
attr_accessor :current
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# @param base [String] base currency the rates are expressed against
|
|
22
|
+
# @param rates [Hash{String=>BigDecimal}] base->code rate map
|
|
23
|
+
def initialize(base:, rates:)
|
|
24
|
+
@base = base.to_s.upcase
|
|
25
|
+
@rates = rates.freeze
|
|
26
|
+
freeze
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# All currency codes available in this snapshot (including the base),
|
|
30
|
+
# sorted — handy for `{% for c in ... %}` style template loops.
|
|
31
|
+
def currencies
|
|
32
|
+
([@base] + @rates.keys).uniq.sort
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Cross-rate from +from+ to +to+ as a BigDecimal, or nil if either
|
|
36
|
+
# currency is absent from the snapshot.
|
|
37
|
+
def rate(from, to)
|
|
38
|
+
from = from.to_s.upcase
|
|
39
|
+
to = to.to_s.upcase
|
|
40
|
+
return BigDecimal(1) if from == to
|
|
41
|
+
|
|
42
|
+
from_rate = base_rate(from)
|
|
43
|
+
to_rate = base_rate(to)
|
|
44
|
+
return nil if from_rate.nil? || to_rate.nil?
|
|
45
|
+
|
|
46
|
+
to_rate / from_rate
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Convert +amount+ from +from+ to +to+ as a BigDecimal, or nil if no
|
|
50
|
+
# rate is available for the pair.
|
|
51
|
+
def convert(amount, from, to)
|
|
52
|
+
r = rate(from, to)
|
|
53
|
+
return nil if r.nil?
|
|
54
|
+
|
|
55
|
+
BigDecimal(amount.to_s) * r
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
# base->iso rate (1 for the base itself); nil if not present.
|
|
61
|
+
def base_rate(iso)
|
|
62
|
+
return BigDecimal(1) if iso == @base
|
|
63
|
+
|
|
64
|
+
@rates[iso]
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "liquid"
|
|
4
|
+
|
|
5
|
+
require_relative "snapshot"
|
|
6
|
+
require_relative "formatter"
|
|
7
|
+
|
|
8
|
+
module Jekyll
|
|
9
|
+
module Unirate
|
|
10
|
+
# Shared parsing/resolution for the UniRate Liquid tags. Each tag takes
|
|
11
|
+
# space-separated arguments that may be string/number literals OR Liquid
|
|
12
|
+
# variables resolved against the render context:
|
|
13
|
+
#
|
|
14
|
+
# {% unirate_rate USD EUR %}
|
|
15
|
+
# {% unirate_convert 100 USD EUR %}
|
|
16
|
+
# {% unirate_price item.price USD EUR %}
|
|
17
|
+
# {% unirate_price 100 USD page.currency %}
|
|
18
|
+
class BaseTag < Liquid::Tag
|
|
19
|
+
def initialize(tag_name, markup, tokens)
|
|
20
|
+
super
|
|
21
|
+
@args = markup.strip.split(/\s+/)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
# Resolve a single argument: a quoted/bare literal, or a context lookup.
|
|
27
|
+
def resolve(arg, context)
|
|
28
|
+
return nil if arg.nil?
|
|
29
|
+
|
|
30
|
+
if arg =~ /\A["'](.*)["']\z/
|
|
31
|
+
::Regexp.last_match(1)
|
|
32
|
+
else
|
|
33
|
+
looked_up = context[arg]
|
|
34
|
+
looked_up.nil? ? arg : looked_up
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def snapshot
|
|
39
|
+
Snapshot.current
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# `{% unirate_rate FROM TO %}` -> numeric rate, or empty string if unknown.
|
|
44
|
+
class RateTag < BaseTag
|
|
45
|
+
def render(context)
|
|
46
|
+
from = resolve(@args[0], context)
|
|
47
|
+
to = resolve(@args[1], context)
|
|
48
|
+
return "" if snapshot.nil?
|
|
49
|
+
|
|
50
|
+
rate = snapshot.rate(from, to)
|
|
51
|
+
rate ? rate.to_f.to_s : ""
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# `{% unirate_convert AMOUNT FROM TO %}` -> converted number (falls back to
|
|
56
|
+
# the input amount when no rate is available).
|
|
57
|
+
class ConvertTag < BaseTag
|
|
58
|
+
def render(context)
|
|
59
|
+
amount = resolve(@args[0], context)
|
|
60
|
+
from = resolve(@args[1], context)
|
|
61
|
+
to = resolve(@args[2], context)
|
|
62
|
+
return amount.to_s if snapshot.nil?
|
|
63
|
+
|
|
64
|
+
converted = snapshot.convert(amount, from, to)
|
|
65
|
+
(converted ? converted.to_f : amount).to_s
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# `{% unirate_price AMOUNT FROM TO %}` -> converted amount formatted in TO.
|
|
70
|
+
class PriceTag < BaseTag
|
|
71
|
+
def render(context)
|
|
72
|
+
amount = resolve(@args[0], context)
|
|
73
|
+
from = resolve(@args[1], context)
|
|
74
|
+
to = resolve(@args[2], context)
|
|
75
|
+
value = snapshot ? (snapshot.convert(amount, from, to) || amount) : amount
|
|
76
|
+
Formatter.format(value, to)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
Liquid::Template.register_tag("unirate_rate", Jekyll::Unirate::RateTag)
|
|
83
|
+
Liquid::Template.register_tag("unirate_convert", Jekyll::Unirate::ConvertTag)
|
|
84
|
+
Liquid::Template.register_tag("unirate_price", Jekyll::Unirate::PriceTag)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "unirate/version"
|
|
4
|
+
|
|
5
|
+
module Jekyll
|
|
6
|
+
# UniRate currency integration for Jekyll. See {Generator}, {Filters} and the
|
|
7
|
+
# tag classes for the build-time and template-side entry points.
|
|
8
|
+
module Unirate
|
|
9
|
+
class << self
|
|
10
|
+
# Display currency used by one-argument helpers; set by {Generator} from
|
|
11
|
+
# `unirate.default_currency` in _config.yml. Defaults to "USD".
|
|
12
|
+
attr_writer :default_currency
|
|
13
|
+
|
|
14
|
+
def default_currency
|
|
15
|
+
@default_currency || "USD"
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
require_relative "unirate/client"
|
|
22
|
+
require_relative "unirate/snapshot"
|
|
23
|
+
require_relative "unirate/formatter"
|
|
24
|
+
require_relative "unirate/config"
|
|
25
|
+
require_relative "unirate/filters"
|
|
26
|
+
require_relative "unirate/tags"
|
|
27
|
+
require_relative "unirate/generator"
|
metadata
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: jekyll-unirate
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Unirate Team
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-06-19 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: jekyll
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '3.7'
|
|
20
|
+
- - "<"
|
|
21
|
+
- !ruby/object:Gem::Version
|
|
22
|
+
version: '5.0'
|
|
23
|
+
type: :runtime
|
|
24
|
+
prerelease: false
|
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
26
|
+
requirements:
|
|
27
|
+
- - ">="
|
|
28
|
+
- !ruby/object:Gem::Version
|
|
29
|
+
version: '3.7'
|
|
30
|
+
- - "<"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '5.0'
|
|
33
|
+
- !ruby/object:Gem::Dependency
|
|
34
|
+
name: rake
|
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '13.0'
|
|
40
|
+
type: :development
|
|
41
|
+
prerelease: false
|
|
42
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '13.0'
|
|
47
|
+
- !ruby/object:Gem::Dependency
|
|
48
|
+
name: rspec
|
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '3.12'
|
|
54
|
+
type: :development
|
|
55
|
+
prerelease: false
|
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '3.12'
|
|
61
|
+
- !ruby/object:Gem::Dependency
|
|
62
|
+
name: rubocop
|
|
63
|
+
requirement: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '1.60'
|
|
68
|
+
type: :development
|
|
69
|
+
prerelease: false
|
|
70
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - "~>"
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '1.60'
|
|
75
|
+
- !ruby/object:Gem::Dependency
|
|
76
|
+
name: webmock
|
|
77
|
+
requirement: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - "~>"
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '3.19'
|
|
82
|
+
type: :development
|
|
83
|
+
prerelease: false
|
|
84
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - "~>"
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '3.19'
|
|
89
|
+
description: A Jekyll plugin backed by the UniRate API (https://unirateapi.com). Fetches
|
|
90
|
+
one exchange-rate snapshot at build time and exposes Liquid filters and tags to
|
|
91
|
+
convert, format, and look up currency rates, with cross-rates derived on demand.
|
|
92
|
+
Fails gracefully so an API blip never breaks the build.
|
|
93
|
+
email:
|
|
94
|
+
- admin@unirateapi.com
|
|
95
|
+
executables: []
|
|
96
|
+
extensions: []
|
|
97
|
+
extra_rdoc_files: []
|
|
98
|
+
files:
|
|
99
|
+
- CHANGELOG.md
|
|
100
|
+
- LICENSE
|
|
101
|
+
- README.md
|
|
102
|
+
- jekyll-unirate.gemspec
|
|
103
|
+
- lib/jekyll-unirate.rb
|
|
104
|
+
- lib/jekyll/unirate.rb
|
|
105
|
+
- lib/jekyll/unirate/client.rb
|
|
106
|
+
- lib/jekyll/unirate/config.rb
|
|
107
|
+
- lib/jekyll/unirate/filters.rb
|
|
108
|
+
- lib/jekyll/unirate/formatter.rb
|
|
109
|
+
- lib/jekyll/unirate/generator.rb
|
|
110
|
+
- lib/jekyll/unirate/snapshot.rb
|
|
111
|
+
- lib/jekyll/unirate/tags.rb
|
|
112
|
+
- lib/jekyll/unirate/version.rb
|
|
113
|
+
homepage: https://github.com/UniRate-API/jekyll-unirate
|
|
114
|
+
licenses:
|
|
115
|
+
- MIT
|
|
116
|
+
metadata:
|
|
117
|
+
homepage_uri: https://github.com/UniRate-API/jekyll-unirate
|
|
118
|
+
source_code_uri: https://github.com/UniRate-API/jekyll-unirate
|
|
119
|
+
bug_tracker_uri: https://github.com/UniRate-API/jekyll-unirate/issues
|
|
120
|
+
changelog_uri: https://github.com/UniRate-API/jekyll-unirate/blob/main/CHANGELOG.md
|
|
121
|
+
documentation_uri: https://unirateapi.com
|
|
122
|
+
rubygems_mfa_required: 'true'
|
|
123
|
+
post_install_message:
|
|
124
|
+
rdoc_options: []
|
|
125
|
+
require_paths:
|
|
126
|
+
- lib
|
|
127
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
128
|
+
requirements:
|
|
129
|
+
- - ">="
|
|
130
|
+
- !ruby/object:Gem::Version
|
|
131
|
+
version: '3.0'
|
|
132
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
133
|
+
requirements:
|
|
134
|
+
- - ">="
|
|
135
|
+
- !ruby/object:Gem::Version
|
|
136
|
+
version: '0'
|
|
137
|
+
requirements: []
|
|
138
|
+
rubygems_version: 3.5.22
|
|
139
|
+
signing_key:
|
|
140
|
+
specification_version: 4
|
|
141
|
+
summary: UniRate currency conversion for Jekyll — Liquid tags & filters.
|
|
142
|
+
test_files: []
|