jekyll-highlight-cards 0.3.1

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.
data/README.md ADDED
@@ -0,0 +1,228 @@
1
+ # jekyll-highlight-cards
2
+
3
+ A Jekyll plugin providing styled card components for links and images with Internet Archive integration.
4
+
5
+ ## Features
6
+
7
+ - **`{% linkcard %}`** - Styled link cards with optional titles and archive links
8
+ - **`{% polaroid %}`** - Polaroid-style image cards with titles, links, and archive support
9
+ - **Markdown Image Sizing** - Extended syntax for image dimensions: `![alt](image.jpg =300x200)`
10
+ - **Internet Archive Integration** - Automatic lookup and archival for both tags
11
+ - **Customizable** - Override HTML templates and CSS styles
12
+
13
+ ## Installation
14
+
15
+ Add to your `Gemfile`:
16
+
17
+ ```ruby
18
+ gem 'jekyll-highlight-cards'
19
+ ```
20
+
21
+ Add to your `_config.yml`:
22
+
23
+ ```yaml
24
+ plugins:
25
+ - jekyll-highlight-cards
26
+ ```
27
+
28
+ Run:
29
+
30
+ ```bash
31
+ bundle install
32
+ ```
33
+
34
+ Add to your `main.scss` file:
35
+
36
+ ```scss
37
+ @import "highlight-cards";
38
+ ```
39
+
40
+ ## Usage
41
+
42
+ ### Linkcard Tag
43
+
44
+ Highlight links:
45
+
46
+ ![Linkcard visual example](docs/linkcard-example.jpg)
47
+
48
+ ```liquid
49
+ {% linkcard https://example.com %}
50
+ {% linkcard https://example.com My Cool Title %}
51
+ {% linkcard https://example.com Title archive:none %}
52
+ {% linkcard https://example.com archive:https://web.archive.org/... %}
53
+ ```
54
+
55
+ **Parameters:**
56
+ | Parameter | Description |
57
+ |------------------|------------------------------------------------------------------|
58
+ | URL | (required, first parameter) |
59
+ | Title | (optional, everything after URL until `archive:`) |
60
+ | `archive:URL` | Explicit archive URL |
61
+ | `archive:none` | Disable archive lookup |
62
+
63
+ ### Polaroid Tag
64
+
65
+ Create polaroid-style image cards:
66
+
67
+ ![Polaroid visual example](docs/polaroid-example.jpg)
68
+
69
+ ```liquid
70
+ {% polaroid /assets/image.jpg %}
71
+ {% polaroid /assets/image.jpg size=300x200 %}
72
+ {% polaroid /assets/image.jpg size=400x title="My Photo" %}
73
+ {% polaroid /assets/image.jpg alt="Screen reader description" %}
74
+ {% polaroid /assets/image.jpg alt="Alt text" title="Visible Title" %}
75
+ {% polaroid /assets/image.jpg title="Photo" link="https://example.com" %}
76
+ {% polaroid {{ page.image }} size=x400 title={{ page.title }} %}
77
+ ```
78
+
79
+ **Parameters:**
80
+ | Parameter | Description |
81
+ |--------------------|----------------------------------------------------------------------------------------------------------|
82
+ | Image URL | (required, first parameter) |
83
+ | `size=WxH` | Image dimensions. Formats: `300x200`, `300x`, `x200`, `300`, `400pxx300px` |
84
+ | `alt="..."` | Alt text for image (for accessibility) |
85
+ | `title="..."` | Title text displayed below image (also used as alt fallback) |
86
+ | `link="..."` | Explicit URL to link to |
87
+ | `archive="..."` | Archive URL or `none` to disable |
88
+
89
+ **Image Alt Text:**
90
+ The `alt` attribute priority: explicit `alt` parameter → `title` parameter → empty string.
91
+
92
+ This allows you to:
93
+ - Set accessible alt text without displaying a visible title
94
+ - Use title as both visual label and screen reader description
95
+ - Separate concerns: detailed alt for accessibility, brief title for display
96
+
97
+ **Link Display:**
98
+ - **No `link` parameter:** Image links to itself, no visible link text shown
99
+ - **With `link` parameter:** Image and visible link text both point to the specified URL
100
+
101
+ **Stacking:**
102
+
103
+ By default, the Polaroids are displayed centered in their available space. Two Polaroids in a row will be [stacked vertically](docs/polaroid-stacked-example.jpg).
104
+
105
+ If you want Polaroids to fill the available width [side-by-side](docs/polaroid-sidebyside-example.jpg), add the following to your `main.scss` file:
106
+
107
+ ```css
108
+ .polaroid-container {
109
+ display: inline-block;
110
+ width: auto;
111
+ }
112
+ ```
113
+
114
+ ### Markdown Image Sizing
115
+
116
+ Add dimensions to Markdown images:
117
+
118
+ ```markdown
119
+ ![Alt text](image.jpg =300x200)
120
+ ![Alt text](image.jpg =400x)
121
+ ![Alt text](image.jpg =x300)
122
+ ![Alt text](image.jpg =400pxx300px)
123
+ ```
124
+
125
+ Sized images are automatically wrapped in links to themselves.
126
+
127
+ ## Configuration
128
+
129
+ ### Internet Archive
130
+
131
+ Enable automatic archive lookup:
132
+
133
+ ```bash
134
+ export JEKYLL_HIGHLIGHT_CARDS_ARCHIVE=1
135
+ ```
136
+
137
+ Or in your shell config:
138
+
139
+ ```bash
140
+ # In .bashrc, .zshrc, etc.
141
+ export JEKYLL_HIGHLIGHT_CARDS_ARCHIVE=1
142
+ ```
143
+
144
+ ### CSS Styles
145
+
146
+ Import defaults and override specific properties:
147
+
148
+ ```scss
149
+ @import "highlight-cards";
150
+
151
+ .link-card {
152
+ border-color: red; // Override
153
+ }
154
+ ```
155
+
156
+ The default style are structural only - they create the shapes but don't set colors, fonts, etc.
157
+ The recommended approach is to use the default styles and then add aesthetics to the provided classes.
158
+
159
+ ### Template Customization
160
+
161
+ If the provided HTML structure doesn't work for you, you can override templates by creating files in your Jekyll site:
162
+
163
+ **Linkcard template:**
164
+ - Create `_includes/highlight-cards/linkcard.html`
165
+
166
+ **Polaroid template:**
167
+ - Create `_includes/highlight-cards/polaroid.html`
168
+
169
+ Templates receive these variables:
170
+
171
+ **Linkcard variables:**
172
+ - `url`, `display_url`, `title`, `archive_url`
173
+ - `escaped_url`, `escaped_display_url`, `escaped_title`, `escaped_archive_url`
174
+
175
+ **Polaroid variables:**
176
+ - `image_url`, `link_url`, `title`, `link_display`, `archive_url`, `width`, `height`
177
+ - `escaped_*` versions of all text fields
178
+
179
+ See default templates in gem's `_includes/` directory for examples.
180
+
181
+ ## Examples
182
+
183
+ ### Blog post with link card
184
+
185
+ ```markdown
186
+ ---
187
+ title: My Blog Post
188
+ ---
189
+
190
+ Check out this cool site:
191
+
192
+ {% linkcard https://jekyllrb.com Jekyll - Simple, blog-aware, static sites %}
193
+
194
+ More content here...
195
+ ```
196
+
197
+ ### Gallery with polaroids
198
+
199
+ ```markdown
200
+ ---
201
+ title: Photo Gallery
202
+ photos:
203
+ - url: /assets/photo1.jpg
204
+ title: Sunset
205
+ - url: /assets/photo2.jpg
206
+ title: Mountains
207
+ ---
208
+
209
+ {% for photo in page.photos %}
210
+ {% polaroid {{ photo.url }} size=300x300 title={{ photo.title }} %}
211
+ {% endfor %}
212
+ ```
213
+
214
+ ### Sized images in Markdown
215
+
216
+ ```markdown
217
+ Here's a large image:
218
+
219
+ ![My Photo](photo.jpg =800x600)
220
+
221
+ And a smaller one:
222
+
223
+ ![Thumbnail](thumb.jpg =150x150)
224
+ ```
225
+
226
+ ## Development
227
+
228
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup and guidelines.
@@ -0,0 +1,13 @@
1
+ <blockquote class="link-card">
2
+ {% if title %}
3
+ <h1>{{ escaped_title }}</h1>
4
+ {% endif %}
5
+ <a href="{{ escaped_url }}" target="_blank" rel="noopener">{{ escaped_display_url }}</a>
6
+ {% if archive_url %}
7
+ <small class="link-card-archive">
8
+ (<a href="{{ escaped_archive_url }}" target="_blank" rel="noopener">archive</a>)
9
+ </small>
10
+ {% endif %}
11
+ </blockquote>
12
+
13
+
@@ -0,0 +1,22 @@
1
+ <div class="polaroid-container">
2
+ <div class="polaroid">
3
+ <a href="{{ escaped_link_url }}"{% unless link_url == image_url %} target="_blank" rel="noopener"{% endunless %}>
4
+ <img src="{{ escaped_image_url }}" alt="{% if alt %}{{ escaped_alt }}{% elsif title %}{{ escaped_title }}{% endif %}"{% if width %} width="{{ width }}"{% endif %}{% if height %} height="{{ height }}"{% endif %} class="polaroid-image">
5
+ </a>
6
+ <div class="polaroid-title">{{ escaped_title }}</div>
7
+ <div class="polaroid-link">
8
+ {% if link_display %}
9
+ <a href="{{ escaped_link_url }}" target="_blank" rel="noopener">{{ escaped_link_display }}</a>
10
+ {% else %}
11
+ &nbsp;
12
+ {% endif %}
13
+ </div>
14
+ <small class="polaroid-archive">
15
+ {% if archive_url %}
16
+ (<a href="{{ escaped_archive_url }}" target="_blank" rel="noopener">archive</a>)
17
+ {% else %}
18
+ &nbsp;
19
+ {% endif %}
20
+ </small>
21
+ </div>
22
+ </div>
@@ -0,0 +1,92 @@
1
+ /*
2
+ * jekyll-highlight-cards Default Styles
3
+ *
4
+ * These styles provide a sensible default appearance for linkcard and polaroid tags.
5
+ * You can override these styles in your Jekyll site by:
6
+ * 1. Not importing this file (fully custom CSS)
7
+ * 2. Importing and overriding specific classes
8
+ * 3. Using higher specificity selectors
9
+ *
10
+ * To use these styles, add to your site's _config.yml or main SCSS:
11
+ * @use "highlight-cards";
12
+ *
13
+ * All styles use low specificity for easy customization.
14
+ */
15
+
16
+ /* ============================================================================
17
+ Linkcard Styles
18
+ ========================================================================= */
19
+
20
+ .link-card {
21
+ padding: 1em 1.25em;
22
+ text-align: center;
23
+ position: relative;
24
+ }
25
+
26
+ .link-card-archive {
27
+ position: absolute;
28
+ right: 0.75rem;
29
+ bottom: 0.5rem;
30
+ }
31
+
32
+ /* ============================================================================
33
+ Polaroid Styles
34
+ ========================================================================= */
35
+
36
+
37
+ .polaroid-container {
38
+ display: block;
39
+ width: 100%;
40
+ text-align: center;
41
+ }
42
+
43
+ .polaroid {
44
+ display: inline-block;
45
+ position: relative;
46
+ border: 1px solid #333;
47
+ padding: 10px 10px 25px 10px;
48
+ margin: 1em;
49
+ background-color: #fff;
50
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
51
+ text-align: center;
52
+ max-width: 100%;
53
+ transition: box-shadow 0.2s ease, transform 0.2s ease;
54
+
55
+ .polaroid-image {
56
+ display: block;
57
+ max-width: 100%;
58
+ border: 1px solid #333;
59
+ margin: 0 auto;
60
+ }
61
+
62
+ .polaroid-title {
63
+ margin-top: 5px;
64
+ padding: 0 10px;
65
+ }
66
+
67
+ .polaroid-link {
68
+ margin-top: 5px;
69
+ padding: 0 10px;
70
+ word-break: break-all;
71
+ }
72
+
73
+ .polaroid-archive {
74
+ position: absolute;
75
+ right: 0.75rem;
76
+ bottom: 0.5rem;
77
+ }
78
+ }
79
+
80
+ /* ============================================================================
81
+ Print Styles
82
+ ========================================================================= */
83
+
84
+ @media print {
85
+ .link-card,
86
+ .polaroid {
87
+ box-shadow: none;
88
+ border: 1px solid #333;
89
+ page-break-inside: avoid;
90
+ }
91
+ }
92
+
Binary file
Binary file
Binary file
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/jekyll-highlight-cards/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "jekyll-highlight-cards"
7
+ spec.version = JekyllHighlightCards::VERSION
8
+ spec.authors = ["Texarkanine"]
9
+ spec.email = ["texarkanine@protonmail.com"]
10
+
11
+ spec.summary = "Jekyll plugin providing linkcard and polaroid Liquid tags with archive integration"
12
+ spec.description = "A Jekyll gem that provides two Liquid tags (linkcard and polaroid) for creating " \
13
+ "styled card components with integrated Internet Archive functionality and image sizing. " \
14
+ "Also includes Markdown image sizing hooks."
15
+ spec.homepage = "https://github.com/texarkanine/jekyll-highlight-cards"
16
+ spec.license = "AGPL-3.0-or-later"
17
+ spec.required_ruby_version = ">= 3.1.0"
18
+
19
+ spec.metadata["homepage_uri"] = spec.homepage
20
+ spec.metadata["source_code_uri"] = spec.homepage
21
+ spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/main/CHANGELOG.md"
22
+ spec.metadata["rubygems_mfa_required"] = "true"
23
+
24
+ # Specify which files should be added to the gem when it is released.
25
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
26
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
27
+ `git ls-files -z`.split("\x0").reject do |f|
28
+ f.match(%r{^(test|spec|features|planning|examples)/})
29
+ end
30
+ end
31
+
32
+ spec.require_paths = ["lib"]
33
+
34
+ # Runtime dependencies
35
+ spec.add_dependency "jekyll", ">= 4.0", "< 5.0"
36
+ spec.add_dependency "liquid", ">= 4.0", "< 5.0"
37
+
38
+ # Development dependencies
39
+ spec.add_development_dependency "bundler", "~> 2.0"
40
+ spec.add_development_dependency "rake", "~> 13.0"
41
+ spec.add_development_dependency "rspec", "~> 3.12"
42
+ spec.add_development_dependency "rubocop", "~> 1.50"
43
+ spec.add_development_dependency "rubocop-rake", "~> 0.6"
44
+ spec.add_development_dependency "rubocop-rspec", "~> 2.20"
45
+ spec.add_development_dependency "simplecov", "~> 0.22"
46
+ spec.add_development_dependency "webmock", "~> 3.18"
47
+ end
@@ -0,0 +1,151 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JekyllHighlightCards
4
+ # Internet Archive integration for automatic URL archival
5
+ #
6
+ # Provides methods for looking up existing archives and submitting
7
+ # URLs to the Internet Archive's Wayback Machine. Results are cached
8
+ # per-site-build to avoid redundant API calls.
9
+ #
10
+ # @example Enable archiving
11
+ # export JEKYLL_HIGHLIGHT_CARDS_ARCHIVE=1
12
+ #
13
+ # @example Enable auto-submission
14
+ # export JEKYLL_HIGHLIGHT_CARDS_ARCHIVE_SAVE=1
15
+ module ArchiveHelper
16
+ # Shared cache for archive URLs across all tag instances
17
+ @archive_cache = {}
18
+
19
+ class << self
20
+ attr_accessor :archive_cache
21
+ end
22
+
23
+ # Get archive URL for a given URL, with caching and optional submission
24
+ #
25
+ # @param url [String] the original URL to archive
26
+ # @return [String, nil] the archive URL, or nil if not found
27
+ def archive_url_for(url)
28
+ ArchiveHelper.archive_cache[url] ||= begin
29
+ log_info("Looking up archive for #{url}")
30
+ archive_url = lookup_archive(url)
31
+ log_info("Archive URL: #{archive_url || ""}")
32
+
33
+ if archive_save_enabled?
34
+ log_info("Submitting to SavePageNow: #{url}")
35
+ archive_url = submit_archive(url) || archive_url
36
+ log_info("SavePageNow archived #{url} -> #{archive_url}")
37
+ end
38
+
39
+ archive_url.to_s.empty? ? nil : archive_url
40
+ end
41
+ rescue StandardError => e
42
+ log_debug("Archive lookup failed for #{url}: #{e.message}")
43
+ nil
44
+ end
45
+
46
+ # Check if archiving is enabled via environment variables
47
+ #
48
+ # @return [Boolean] true if archiving is enabled
49
+ def archive_enabled?
50
+ ENV["JEKYLL_HIGHLIGHT_CARDS_ARCHIVE"] == "1" || archive_save_enabled?
51
+ end
52
+
53
+ # Check if SavePageNow submission is enabled
54
+ #
55
+ # @return [Boolean] true if submission is enabled
56
+ def archive_save_enabled?
57
+ ENV["JEKYLL_HIGHLIGHT_CARDS_ARCHIVE_SAVE"] == "1"
58
+ end
59
+
60
+ # Get User-Agent string for archive HTTP requests
61
+ #
62
+ # @return [String] User-Agent header value
63
+ def archive_user_agent
64
+ ENV["JEKYLL_HIGHLIGHT_CARDS_ARCHIVE_UA"] ||
65
+ "jekyll:highlight-cards (+#{ENV.fetch("JEKYLL_HIGHLIGHT_CARDS_ARCHIVE_CONTACT", "mailto:unknown")})"
66
+ end
67
+
68
+ private
69
+
70
+ # Look up the latest archived snapshot for a URL via Internet Archive CDX API
71
+ #
72
+ # @param url [String] the URL to look up
73
+ # @return [String, nil] the archive URL if found, nil otherwise
74
+ def lookup_archive(url)
75
+ log_debug("lookup_archive(#{url})")
76
+
77
+ encoded_url = URI.encode_www_form_component(url)
78
+ cdx_url_str = "https://web.archive.org/cdx/search/cdx?url=#{encoded_url}&output=json&filter=statuscode:200&limit=-1&fl=timestamp,original"
79
+ cdx_url = URI.parse(cdx_url_str)
80
+
81
+ log_debug("CDX lookup URL: #{cdx_url_str}")
82
+
83
+ response = Net::HTTP.start(
84
+ cdx_url.host,
85
+ cdx_url.port,
86
+ use_ssl: cdx_url.scheme == "https",
87
+ open_timeout: 10,
88
+ read_timeout: 30
89
+ ) do |http|
90
+ http.request(Net::HTTP::Get.new(cdx_url.request_uri))
91
+ end
92
+
93
+ unless response.is_a?(Net::HTTPSuccess)
94
+ log_debug("CDX lookup failed: #{response.code} #{response.message}")
95
+ return nil
96
+ end
97
+
98
+ log_debug("CDX lookup found archived page...")
99
+ rows = JSON.parse(response.body)
100
+
101
+ # First row is header, so we need at least 2 rows
102
+ return nil if rows.length <= 1
103
+
104
+ latest = rows.last
105
+ timestamp = latest[0]
106
+ archive_url = "https://web.archive.org/web/#{timestamp}/#{url}"
107
+
108
+ log_debug("CDX lookup found archived page: #{archive_url}")
109
+ archive_url
110
+ rescue StandardError => e
111
+ log_debug("CDX lookup error for #{url}: #{e.message}")
112
+ nil
113
+ end
114
+
115
+ # Submit a URL to Internet Archive SavePageNow service
116
+ #
117
+ # @param url [String] the URL to submit for archiving
118
+ # @return [String, nil] the archive URL if successful, nil otherwise
119
+ def submit_archive(url)
120
+ log_debug("submit_archive(#{url})")
121
+
122
+ encoded_url = URI.encode_www_form_component(url)
123
+ save_url = URI.parse("https://web.archive.org/save/#{encoded_url}")
124
+
125
+ response = Net::HTTP.start(
126
+ save_url.host,
127
+ save_url.port,
128
+ use_ssl: save_url.scheme == "https",
129
+ open_timeout: 10,
130
+ read_timeout: 30
131
+ ) do |http|
132
+ req = Net::HTTP::Get.new(save_url.request_uri, { "User-Agent" => archive_user_agent })
133
+ http.request(req)
134
+ end
135
+
136
+ location = response["content-location"]
137
+
138
+ if location && !location.empty?
139
+ archive_url = "https://web.archive.org#{location}"
140
+ log_info("SavePageNow archived #{url} -> #{archive_url}")
141
+ archive_url
142
+ else
143
+ log_debug("Archive submission returned no location for #{url}")
144
+ nil
145
+ end
146
+ rescue StandardError => e
147
+ log_debug("Archive submission error for #{url}: #{e.message}")
148
+ nil
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JekyllHighlightCards
4
+ # Parse dimension specifications in "WxH" format
5
+ #
6
+ # Supports multiple formats for specifying image dimensions:
7
+ # - `WIDTHxHEIGHT`: Both dimensions (e.g., "300x200")
8
+ # - `WIDTHx`: Width only (e.g., "300x")
9
+ # - `xHEIGHT`: Height only (e.g., "x200")
10
+ # - `WIDTH`: Width only shorthand (e.g., "300")
11
+ # - Units: Supports px, em, %, etc. (e.g., "400pxx300px")
12
+ #
13
+ # @example
14
+ # include DimensionParser
15
+ # width, height = parse_dimensions("300x200") #=> ["300", "200"]
16
+ module DimensionParser
17
+ # Parse dimension string into width and height components
18
+ #
19
+ # @param dim_str [String] dimension string (e.g., "300x200", "300x", "x200", "300")
20
+ # @return [Array<String, nil>] array of [width, height] where nil indicates unspecified
21
+ #
22
+ # @example
23
+ # parse_dimensions("300x200") #=> ["300", "200"]
24
+ # parse_dimensions("300x") #=> ["300", nil]
25
+ # parse_dimensions("x200") #=> [nil, "200"]
26
+ # parse_dimensions("300") #=> ["300", nil]
27
+ # parse_dimensions("400px") #=> ["400px", nil]
28
+ def parse_dimensions(dim_str)
29
+ return [nil, nil] if dim_str.nil? || dim_str.empty?
30
+
31
+ # Determine the separator:
32
+ # - If "xx" is present, the SECOND 'x' is the separator (first 'x' is part of the dimension)
33
+ # Example: "400pxx300px" → width="400px", height="300px"
34
+ # - Otherwise, check if there's an 'x' that's a separator (not part of a unit like "px")
35
+ # An 'x' is a separator if it's at the end, followed by a digit, or at the start
36
+ if dim_str.include?("xx")
37
+ # Find the position of "xx"
38
+ idx = dim_str.index("xx")
39
+ # Split at the second 'x' (include first 'x' in width, skip both 'x's for height)
40
+ width = dim_str[0..idx].empty? ? nil : dim_str[0..idx]
41
+ height = dim_str[(idx + 2)..]
42
+ height = nil if height.nil? || height.empty?
43
+ [width, height]
44
+ elsif dim_str =~ /x\d/ || dim_str =~ /(?<![a-z])x$/i || dim_str.start_with?("x")
45
+ # Single 'x' separator in one of these cases:
46
+ # 1. Followed by a digit: "300x200", "10emx20em"
47
+ # 2. At end but NOT preceded by a letter: "300x"
48
+ # 3. At start: "x200"
49
+ # This matches "300x", "300x200", "x200", "10emx20em", but NOT "400px"
50
+ parts = dim_str.split("x", 2)
51
+ width = parts[0].empty? ? nil : parts[0]
52
+ height = parts[1].nil? || parts[1].empty? ? nil : parts[1]
53
+ [width, height]
54
+ else
55
+ # No separator found - treat as width only
56
+ [dim_str, nil]
57
+ end
58
+ end
59
+
60
+ module_function :parse_dimensions
61
+ end
62
+ end