jekyll-highlight-cards 2.0.0 → 2.2.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 +4 -4
- data/CHANGELOG.md +19 -0
- data/README.md +36 -0
- data/jekyll-highlight-cards.gemspec +4 -2
- data/lib/jekyll-highlight-cards/archive_helper.rb +73 -54
- data/lib/jekyll-highlight-cards/commands/freeze_archives.rb +178 -0
- data/lib/jekyll-highlight-cards/dimension_parser.rb +10 -18
- data/lib/jekyll-highlight-cards/expression_evaluator.rb +22 -23
- data/lib/jekyll-highlight-cards/freeze_archives/archive_inserter.rb +72 -0
- data/lib/jekyll-highlight-cards/freeze_archives/markup_analyzer.rb +59 -0
- data/lib/jekyll-highlight-cards/freeze_archives/tag_locator.rb +32 -0
- data/lib/jekyll-highlight-cards/image_sizing_hooks.rb +53 -62
- data/lib/jekyll-highlight-cards/linkcard_markup.rb +83 -0
- data/lib/jekyll-highlight-cards/linkcard_tag.rb +13 -92
- data/lib/jekyll-highlight-cards/polaroid_markup.rb +81 -0
- data/lib/jekyll-highlight-cards/polaroid_tag.rb +43 -119
- data/lib/jekyll-highlight-cards/template_renderer.rb +22 -20
- data/lib/jekyll-highlight-cards/version.rb +1 -1
- data/lib/jekyll-highlight-cards.rb +6 -0
- metadata +39 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 48a4ccd9a729f6d5db3c458bdf9311abe63b595b370cb6577994e7c0da53d763
|
|
4
|
+
data.tar.gz: 4805a92c237413bcc17ddd0c2d1166bd5c7e606a9c2287b8f6e99939d42d6639
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3c01b9b3dc7adb48b1e20a5dce6af56f0ba2e0e1db4e490c7024c066a8ee8cf3dc9da0e3f65637aec448ecd3bd67b39169f4719536f0fd27352ab46a76303a95
|
|
7
|
+
data.tar.gz: 4d88a809394b5b4d77a73f385358d05c532ee38f0c22f26dada841b2c5aff61915fc4486bb61ab83ffc5c282b2b1226b7fde8c75c4654260f280e6e83d76193a
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [2.2.0](https://github.com/Texarkanine/jekyll-highlight-cards/compare/v2.1.0...v2.2.0) (2026-08-02)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* freeze-archives command and highlight_cards.noarchive skips ([#56](https://github.com/Texarkanine/jekyll-highlight-cards/issues/56)) ([a50d109](https://github.com/Texarkanine/jekyll-highlight-cards/commit/a50d109807a0709c47aeebab021b5b49bbd35d8f))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* **archive:** skip non-archivable URLs [[#53](https://github.com/Texarkanine/jekyll-highlight-cards/issues/53)] ([#54](https://github.com/Texarkanine/jekyll-highlight-cards/issues/54)) ([928e6d8](https://github.com/Texarkanine/jekyll-highlight-cards/commit/928e6d8268318dbca85ec5d8143b1a1b67f6275a))
|
|
14
|
+
|
|
15
|
+
## [2.1.0](https://github.com/Texarkanine/jekyll-highlight-cards/compare/v2.0.0...v2.1.0) (2026-07-19)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Features
|
|
19
|
+
|
|
20
|
+
* **test:** add Mutant mutation testing at 100% coverage ([#49](https://github.com/Texarkanine/jekyll-highlight-cards/issues/49)) ([e4e63dc](https://github.com/Texarkanine/jekyll-highlight-cards/commit/e4e63dc259ee767d0db31cb86e1b03e298f76f0d))
|
|
21
|
+
|
|
3
22
|
## [2.0.0](https://github.com/Texarkanine/jekyll-highlight-cards/compare/v1.1.2...v2.0.0) (2026-01-18)
|
|
4
23
|
|
|
5
24
|
|
data/README.md
CHANGED
|
@@ -11,6 +11,7 @@ A Jekyll plugin providing styled card components for links and images with Inter
|
|
|
11
11
|
- **`{% polaroid %}`** - Polaroid-style image cards with titles, links, and archive support
|
|
12
12
|
- **Markdown Image Sizing** - Extended syntax for image dimensions: ``
|
|
13
13
|
- **Internet Archive Integration** - Automatic lookup and archival for both tags
|
|
14
|
+
- **`jekyll freeze-archives`** - Opt-in command to freeze archive URLs into source
|
|
14
15
|
- **Customizable** - Override HTML templates and CSS styles
|
|
15
16
|
|
|
16
17
|
## Installation
|
|
@@ -148,6 +149,41 @@ Or in your shell config:
|
|
|
148
149
|
export JEKYLL_HIGHLIGHT_CARDS_ARCHIVE=1
|
|
149
150
|
```
|
|
150
151
|
|
|
152
|
+
| Flag | Description |
|
|
153
|
+
|------|-------------|
|
|
154
|
+
| `JEKYLL_HIGHLIGHT_CARDS_ARCHIVE` | Enable build-time lookup to see if there is an IA snapshot available |
|
|
155
|
+
| `JEKYLL_HIGHLIGHT_CARDS_ARCHIVE_SAVE` | Also submit the URL to SavePageNow (runs even when CDX already found a snapshot) |
|
|
156
|
+
|
|
157
|
+
You can selectively skip archive attempts for URLs that match site-configured regexes. Patterns are matched against the full URL string; use anchors when needed.
|
|
158
|
+
|
|
159
|
+
```yaml
|
|
160
|
+
highlight_cards:
|
|
161
|
+
noarchive:
|
|
162
|
+
- ^https?://x\.com/.*
|
|
163
|
+
- \Ahttps://something\.example\.com/.*
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Freeze Archives Into Source
|
|
167
|
+
|
|
168
|
+
To look up Internet Archive snapshots *once* (instead of on each build) and write them into your source tags, run the `freeze-archives` Jekyll subcommand:
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
# Rehearse first — reports planned edits, writes nothing
|
|
172
|
+
bundle exec jekyll freeze-archives --dry-run
|
|
173
|
+
|
|
174
|
+
# Write archive: / archive= into eligible tags
|
|
175
|
+
bundle exec jekyll freeze-archives
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Review the diff, then commit. The command does **not** commit for you, and it does **not** run as part of `jekyll build`.
|
|
179
|
+
|
|
180
|
+
**How it differs from build-time lookup**
|
|
181
|
+
|
|
182
|
+
- Invoking `freeze-archives` performs CDX lookup — you do **not** need `JEKYLL_HIGHLIGHT_CARDS_ARCHIVE=1`.
|
|
183
|
+
- SavePageNow stays gated: set `JEKYLL_HIGHLIGHT_CARDS_ARCHIVE_SAVE=1` or pass `--save`.
|
|
184
|
+
- Only **literal** http(s) URLs are frozen. Liquid targets like `{{ page.url }}` are skipped; build-time auto-lookup still covers those when enabled.
|
|
185
|
+
- Tags that already have `archive:` / `archive=` / `archive:none` are left alone.
|
|
186
|
+
|
|
151
187
|
### CSS Styles
|
|
152
188
|
|
|
153
189
|
**Default usage:**
|
|
@@ -40,12 +40,14 @@ Gem::Specification.new do |spec|
|
|
|
40
40
|
|
|
41
41
|
# Development dependencies
|
|
42
42
|
spec.add_development_dependency "bundler", "~> 2.0"
|
|
43
|
+
spec.add_development_dependency "mutant", "~> 0.16"
|
|
44
|
+
spec.add_development_dependency "mutant-rspec", "~> 0.16"
|
|
43
45
|
spec.add_development_dependency "rake", "~> 13.0"
|
|
44
46
|
spec.add_development_dependency "rspec", "~> 3.12"
|
|
45
47
|
spec.add_development_dependency "rubocop", "~> 1.50"
|
|
46
48
|
spec.add_development_dependency "rubocop-rake", "~> 0.6"
|
|
47
49
|
spec.add_development_dependency "rubocop-rspec", "~> 3.8"
|
|
48
|
-
spec.add_development_dependency "simplecov", "~> 0
|
|
49
|
-
spec.add_development_dependency "simplecov-cobertura", "~>
|
|
50
|
+
spec.add_development_dependency "simplecov", "~> 1.0"
|
|
51
|
+
spec.add_development_dependency "simplecov-cobertura", "~> 4.0"
|
|
50
52
|
spec.add_development_dependency "webmock", "~> 3.18"
|
|
51
53
|
end
|
|
@@ -17,30 +17,24 @@ module JekyllHighlightCards
|
|
|
17
17
|
@archive_cache = {}
|
|
18
18
|
|
|
19
19
|
class << self
|
|
20
|
-
attr_accessor :archive_cache
|
|
20
|
+
attr_accessor :archive_cache, :noarchive_regexp_cache
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
# Get archive URL for a given URL, with caching and optional submission
|
|
24
24
|
#
|
|
25
25
|
# @param url [String] the original URL to archive
|
|
26
|
+
# @param site [Jekyll::Site, nil] optional site for +highlight_cards.noarchive+ patterns
|
|
26
27
|
# @return [String, nil] the archive URL, or nil if not found
|
|
27
|
-
def archive_url_for(url)
|
|
28
|
+
def archive_url_for(url, site: nil)
|
|
29
|
+
return nil unless archiveable_url?(url, site: site)
|
|
30
|
+
|
|
28
31
|
ArchiveHelper.archive_cache[url] ||= begin
|
|
29
|
-
log_info("Looking up archive for #{url}")
|
|
30
32
|
archive_url = lookup_archive(url)
|
|
31
|
-
log_info("Archive URL: #{archive_url || ""}")
|
|
32
33
|
|
|
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
|
|
34
|
+
archive_url = submit_archive(url) || archive_url if archive_save_enabled?
|
|
38
35
|
|
|
39
|
-
archive_url
|
|
36
|
+
archive_url
|
|
40
37
|
end
|
|
41
|
-
rescue StandardError => e
|
|
42
|
-
log_debug("Archive lookup failed for #{url}: #{e.message}")
|
|
43
|
-
nil
|
|
44
38
|
end
|
|
45
39
|
|
|
46
40
|
# Check if archiving is enabled via environment variables
|
|
@@ -67,19 +61,60 @@ module JekyllHighlightCards
|
|
|
67
61
|
|
|
68
62
|
private
|
|
69
63
|
|
|
64
|
+
# Whether +url+ is eligible for CDX lookup / SavePageNow submission.
|
|
65
|
+
#
|
|
66
|
+
# Requires an absolute http(s) URI with a host. Rejects Internet Archive
|
|
67
|
+
# hosts so Wayback / archive.org URLs are not re-submitted. Also rejects
|
|
68
|
+
# URLs matching any +highlight_cards.noarchive+ regex on +site+ config.
|
|
69
|
+
#
|
|
70
|
+
# @param url [String] candidate URL
|
|
71
|
+
# @param site [Jekyll::Site, nil] optional site for +highlight_cards.noarchive+
|
|
72
|
+
# @return [Boolean] true if the URL may be archived
|
|
73
|
+
def archiveable_url?(url, site: nil)
|
|
74
|
+
uri = URI.parse(url.to_s)
|
|
75
|
+
return false unless uri.is_a?(URI::HTTP) && uri.host
|
|
76
|
+
return false if %w[archive.org web.archive.org].include?(uri.host.downcase)
|
|
77
|
+
return false if noarchive_match?(url, site)
|
|
78
|
+
|
|
79
|
+
true
|
|
80
|
+
rescue URI::InvalidURIError
|
|
81
|
+
false
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Whether +url+ matches any +highlight_cards.noarchive+ pattern on +site+.
|
|
85
|
+
#
|
|
86
|
+
# @param url [String] candidate URL (full string matched)
|
|
87
|
+
# @param site [Jekyll::Site, nil]
|
|
88
|
+
# @return [Boolean]
|
|
89
|
+
# @raise [RegexpError] when a configured pattern is invalid
|
|
90
|
+
def noarchive_match?(url, site)
|
|
91
|
+
noarchive_patterns_for(site).any? { |pattern| pattern.match?(url.to_s) }
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Compile +highlight_cards.noarchive+ patterns from site config (memoized per site).
|
|
95
|
+
#
|
|
96
|
+
# @param site [Jekyll::Site, nil]
|
|
97
|
+
# @return [Array<Regexp>]
|
|
98
|
+
def noarchive_patterns_for(site)
|
|
99
|
+
return [] unless site
|
|
100
|
+
|
|
101
|
+
raw = site.config.dig("highlight_cards", "noarchive")
|
|
102
|
+
return [] if raw.nil? || Array(raw).empty?
|
|
103
|
+
|
|
104
|
+
cache = (ArchiveHelper.noarchive_regexp_cache ||= {})
|
|
105
|
+
cache_key = [site.object_id, raw]
|
|
106
|
+
cache[cache_key] ||= Array(raw).map { |pattern| Regexp.new(pattern.to_s) }
|
|
107
|
+
end
|
|
108
|
+
|
|
70
109
|
# Look up the latest archived snapshot for a URL via Internet Archive CDX API
|
|
71
110
|
#
|
|
72
111
|
# @param url [String] the URL to look up
|
|
73
112
|
# @return [String, nil] the archive URL if found, nil otherwise
|
|
74
113
|
def lookup_archive(url)
|
|
75
|
-
log_debug("lookup_archive(#{url})")
|
|
76
|
-
|
|
77
114
|
encoded_url = URI.encode_www_form_component(url)
|
|
78
115
|
cdx_url_str = "https://web.archive.org/cdx/search/cdx?url=#{encoded_url}&output=json&filter=statuscode:200&limit=-1&fl=timestamp,original"
|
|
79
116
|
cdx_url = URI.parse(cdx_url_str)
|
|
80
117
|
|
|
81
|
-
log_debug("CDX lookup URL: #{cdx_url_str}")
|
|
82
|
-
|
|
83
118
|
response = Net::HTTP.start(
|
|
84
119
|
cdx_url.host,
|
|
85
120
|
cdx_url.port,
|
|
@@ -90,25 +125,15 @@ module JekyllHighlightCards
|
|
|
90
125
|
http.request(Net::HTTP::Get.new(cdx_url.request_uri))
|
|
91
126
|
end
|
|
92
127
|
|
|
93
|
-
unless response.is_a?(Net::HTTPSuccess)
|
|
94
|
-
log_debug("CDX lookup failed: #{response.code} #{response.message}")
|
|
95
|
-
return nil
|
|
96
|
-
end
|
|
128
|
+
return nil unless response.is_a?(Net::HTTPSuccess)
|
|
97
129
|
|
|
98
|
-
log_debug("CDX lookup found archived page...")
|
|
99
130
|
rows = JSON.parse(response.body)
|
|
100
131
|
|
|
101
|
-
|
|
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}"
|
|
132
|
+
return nil if rows.size < 2
|
|
107
133
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
rescue StandardError
|
|
111
|
-
log_debug("CDX lookup error for #{url}: #{e.message}")
|
|
134
|
+
timestamp, = rows.last
|
|
135
|
+
"https://web.archive.org/web/#{timestamp}/#{url}"
|
|
136
|
+
rescue StandardError
|
|
112
137
|
nil
|
|
113
138
|
end
|
|
114
139
|
|
|
@@ -117,35 +142,29 @@ module JekyllHighlightCards
|
|
|
117
142
|
# @param url [String] the URL to submit for archiving
|
|
118
143
|
# @return [String, nil] the archive URL if successful, nil otherwise
|
|
119
144
|
def submit_archive(url)
|
|
120
|
-
log_debug("submit_archive(#{url})")
|
|
121
|
-
|
|
122
145
|
encoded_url = URI.encode_www_form_component(url)
|
|
123
146
|
save_url = URI.parse("https://web.archive.org/save/#{encoded_url}")
|
|
124
147
|
|
|
125
|
-
response =
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
148
|
+
response = begin
|
|
149
|
+
Net::HTTP.start(
|
|
150
|
+
save_url.host,
|
|
151
|
+
save_url.port,
|
|
152
|
+
use_ssl: save_url.scheme == "https",
|
|
153
|
+
open_timeout: 10,
|
|
154
|
+
read_timeout: 30
|
|
155
|
+
) do |http|
|
|
156
|
+
req = Net::HTTP::Get.new(save_url.request_uri, { "User-Agent" => archive_user_agent })
|
|
157
|
+
http.request(req)
|
|
158
|
+
end
|
|
159
|
+
rescue StandardError
|
|
160
|
+
nil
|
|
134
161
|
end
|
|
162
|
+
return nil if response.nil?
|
|
135
163
|
|
|
136
164
|
location = response["content-location"]
|
|
165
|
+
return nil if location.to_s.empty?
|
|
137
166
|
|
|
138
|
-
|
|
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
|
|
167
|
+
"https://web.archive.org#{location}"
|
|
149
168
|
end
|
|
150
169
|
end
|
|
151
170
|
end
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JekyllHighlightCards
|
|
4
|
+
module Commands
|
|
5
|
+
# Opt-in Jekyll subcommand: +jekyll freeze-archives+
|
|
6
|
+
#
|
|
7
|
+
# Scans site source for archive-eligible +linkcard+ / +polaroid+ tags that
|
|
8
|
+
# lack an encoded archive, looks up Internet Archive, and surgically inserts
|
|
9
|
+
# successful hits into source. Does not run during +jekyll build+.
|
|
10
|
+
#
|
|
11
|
+
# @see https://jekyllrb.com/docs/plugins/commands/
|
|
12
|
+
class FreezeArchives < Jekyll::Command
|
|
13
|
+
class << self
|
|
14
|
+
# Register the +freeze-archives+ Mercenary subcommand
|
|
15
|
+
#
|
|
16
|
+
# @param prog [Mercenary::Program] the Jekyll CLI program
|
|
17
|
+
# @return [void]
|
|
18
|
+
def init_with_program(prog)
|
|
19
|
+
prog.command(:"freeze-archives") do |c|
|
|
20
|
+
c.syntax "freeze-archives [options]"
|
|
21
|
+
c.description "Freeze Internet Archive URLs into highlight-card source tags"
|
|
22
|
+
|
|
23
|
+
c.option "dry_run", "--dry-run", "Report planned edits without writing files"
|
|
24
|
+
c.option "save", "--save", "Enable SavePageNow when CDX has no snapshot"
|
|
25
|
+
c.option "config", "--config CONFIG_FILE[,CONFIG_FILE2,...]", Array,
|
|
26
|
+
"Custom configuration file"
|
|
27
|
+
c.option "source", "-s", "--source SOURCE", "Custom source directory"
|
|
28
|
+
|
|
29
|
+
c.action do |_args, options|
|
|
30
|
+
process(options)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Run freeze-archives against a configured site
|
|
36
|
+
#
|
|
37
|
+
# @param options [Hash] Mercenary/Jekyll options (+dry_run+, +save+, +source+, …)
|
|
38
|
+
# @return [Hash] summary counts +:frozen+, +:skipped+, +:miss+, +:written+
|
|
39
|
+
def process(options)
|
|
40
|
+
previous_save = ENV.fetch("JEKYLL_HIGHLIGHT_CARDS_ARCHIVE_SAVE", nil)
|
|
41
|
+
previous_log_level = Jekyll.logger.level
|
|
42
|
+
ENV["JEKYLL_HIGHLIGHT_CARDS_ARCHIVE_SAVE"] = "1" if options["save"]
|
|
43
|
+
|
|
44
|
+
site = Jekyll::Site.new(configuration_from_options(options))
|
|
45
|
+
# Hand-run tool: keep progress visible even if the site sets quiet: true
|
|
46
|
+
Jekyll.logger.log_level = :info unless options["quiet"]
|
|
47
|
+
site.read
|
|
48
|
+
|
|
49
|
+
summary = Runner.new(
|
|
50
|
+
dry_run: options["dry_run"],
|
|
51
|
+
logger: Jekyll.logger
|
|
52
|
+
).run(site)
|
|
53
|
+
|
|
54
|
+
Jekyll.logger.info "freeze-archives:",
|
|
55
|
+
"frozen=#{summary[:frozen]} skipped=#{summary[:skipped]} " \
|
|
56
|
+
"miss=#{summary[:miss]} written=#{summary[:written]}"
|
|
57
|
+
summary
|
|
58
|
+
ensure
|
|
59
|
+
restore_save_env(previous_save, options)
|
|
60
|
+
Jekyll.logger.log_level = previous_log_level if previous_log_level
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def restore_save_env(previous_save, options)
|
|
66
|
+
return unless options && options["save"]
|
|
67
|
+
|
|
68
|
+
if previous_save.nil?
|
|
69
|
+
ENV.delete("JEKYLL_HIGHLIGHT_CARDS_ARCHIVE_SAVE")
|
|
70
|
+
else
|
|
71
|
+
ENV["JEKYLL_HIGHLIGHT_CARDS_ARCHIVE_SAVE"] = previous_save
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Orchestrates locate → analyze → lookup → insert for one site
|
|
77
|
+
class Runner
|
|
78
|
+
include ArchiveHelper
|
|
79
|
+
|
|
80
|
+
def initialize(dry_run:, logger:)
|
|
81
|
+
@dry_run = dry_run
|
|
82
|
+
@logger = logger
|
|
83
|
+
@site = nil
|
|
84
|
+
@site_source = nil
|
|
85
|
+
@locator = JekyllHighlightCards::FreezeArchives::TagLocator.new
|
|
86
|
+
@inserter = JekyllHighlightCards::FreezeArchives::ArchiveInserter.new
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# @param site [Jekyll::Site]
|
|
90
|
+
# @return [Hash] summary counts
|
|
91
|
+
def run(site)
|
|
92
|
+
@site = site
|
|
93
|
+
@site_source = site.source
|
|
94
|
+
@analyzer = JekyllHighlightCards::FreezeArchives::MarkupAnalyzer.new(site: site)
|
|
95
|
+
summary = { frozen: 0, skipped: 0, miss: 0, written: 0 }
|
|
96
|
+
|
|
97
|
+
source_files(site).each do |path|
|
|
98
|
+
process_file(path, summary)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
summary
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
private
|
|
105
|
+
|
|
106
|
+
def source_files(site)
|
|
107
|
+
paths = site.pages.map { |page| resolve_source_path(site, page.path) }
|
|
108
|
+
site.collections.each_value do |collection|
|
|
109
|
+
paths.concat(collection.docs.map { |doc| resolve_source_path(site, doc.path) })
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
paths.compact.uniq.select do |path|
|
|
113
|
+
File.file?(path) && path.start_with?(site.source)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Jekyll::Page#path is source-relative; Document#path is absolute.
|
|
118
|
+
def resolve_source_path(site, path)
|
|
119
|
+
return nil if path.to_s.empty?
|
|
120
|
+
return path if File.absolute_path?(path)
|
|
121
|
+
|
|
122
|
+
site.in_source_dir(path)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def process_file(path, summary)
|
|
126
|
+
content = File.read(path)
|
|
127
|
+
spans = @locator.locate(content)
|
|
128
|
+
return if spans.empty?
|
|
129
|
+
|
|
130
|
+
edits = []
|
|
131
|
+
spans.each do |span|
|
|
132
|
+
candidate = @analyzer.analyze(span[:tag], span[:markup])
|
|
133
|
+
unless candidate
|
|
134
|
+
summary[:skipped] += 1
|
|
135
|
+
next
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
target = candidate[:target_url]
|
|
139
|
+
@logger.info "freeze-archives:", "Looking up archive for #{target}"
|
|
140
|
+
archive_url = archive_url_for(target, site: @site)
|
|
141
|
+
unless archive_url
|
|
142
|
+
summary[:miss] += 1
|
|
143
|
+
@logger.info "freeze-archives:", "no archive for #{target}"
|
|
144
|
+
next
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
summary[:frozen] += 1
|
|
148
|
+
verb = @dry_run ? "would freeze" : "frozen"
|
|
149
|
+
@logger.info "freeze-archives:",
|
|
150
|
+
"#{verb}: #{relative_source_path(path)} ← #{archive_url}"
|
|
151
|
+
edits << { span: span, archive_url: archive_url }
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
return if edits.empty? || @dry_run
|
|
155
|
+
|
|
156
|
+
# Apply from end of file so earlier ranges stay valid
|
|
157
|
+
updated = content
|
|
158
|
+
edits.sort_by { |e| -e[:span][:range].begin }.each do |edit|
|
|
159
|
+
updated = @inserter.insert(updated, edit[:span], edit[:archive_url])
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
return if updated == content
|
|
163
|
+
|
|
164
|
+
File.write(path, updated)
|
|
165
|
+
summary[:written] += 1
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def relative_source_path(path)
|
|
169
|
+
source = @site_source.to_s
|
|
170
|
+
return path if source.empty?
|
|
171
|
+
|
|
172
|
+
prefix = source.end_with?("/") ? source : "#{source}/"
|
|
173
|
+
path.start_with?(prefix) ? path.delete_prefix(prefix) : path
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
@@ -11,8 +11,7 @@ module JekyllHighlightCards
|
|
|
11
11
|
# - Units: Supports px, em, %, etc. (e.g., "400pxx300px")
|
|
12
12
|
#
|
|
13
13
|
# @example
|
|
14
|
-
#
|
|
15
|
-
# width, height = parse_dimensions("300x200") #=> ["300", "200"]
|
|
14
|
+
# width, height = DimensionParser.parse_dimensions("300x200") #=> ["300", "200"]
|
|
16
15
|
module DimensionParser
|
|
17
16
|
# Parse dimension string into width and height components
|
|
18
17
|
#
|
|
@@ -25,7 +24,7 @@ module JekyllHighlightCards
|
|
|
25
24
|
# parse_dimensions("x200") #=> [nil, "200"]
|
|
26
25
|
# parse_dimensions("300") #=> ["300", nil]
|
|
27
26
|
# parse_dimensions("400px") #=> ["400px", nil]
|
|
28
|
-
def parse_dimensions(dim_str)
|
|
27
|
+
def self.parse_dimensions(dim_str)
|
|
29
28
|
return [nil, nil] if dim_str.nil? || dim_str.empty?
|
|
30
29
|
|
|
31
30
|
# Determine the separator:
|
|
@@ -34,29 +33,22 @@ module JekyllHighlightCards
|
|
|
34
33
|
# - Otherwise, check if there's an 'x' that's a separator (not part of a unit like "px")
|
|
35
34
|
# An 'x' is a separator if it's at the end, followed by a digit, or at the start
|
|
36
35
|
if dim_str.include?("xx")
|
|
37
|
-
# Find the position of "xx"
|
|
38
36
|
idx = dim_str.index("xx")
|
|
39
|
-
|
|
40
|
-
width = dim_str[0..idx].empty? ? nil : dim_str[0..idx]
|
|
37
|
+
width = dim_str[...(idx + 1)]
|
|
41
38
|
height = dim_str[(idx + 2)..]
|
|
42
|
-
height = nil if height.
|
|
39
|
+
height = nil if height.empty?
|
|
43
40
|
[width, height]
|
|
44
|
-
elsif dim_str
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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]
|
|
41
|
+
elsif dim_str.match?(/x\d|(?<![a-z])x\z/) || dim_str.start_with?("x")
|
|
42
|
+
width_str, height_str = dim_str.split("x", 2)
|
|
43
|
+
width = width_str
|
|
44
|
+
width = nil if width.empty?
|
|
45
|
+
height = height_str
|
|
46
|
+
height = nil if height.empty?
|
|
53
47
|
[width, height]
|
|
54
48
|
else
|
|
55
49
|
# No separator found - treat as width only
|
|
56
50
|
[dim_str, nil]
|
|
57
51
|
end
|
|
58
52
|
end
|
|
59
|
-
|
|
60
|
-
module_function :parse_dimensions
|
|
61
53
|
end
|
|
62
54
|
end
|
|
@@ -19,14 +19,11 @@ module JekyllHighlightCards
|
|
|
19
19
|
# @param allow_nil [Boolean] whether to allow nil results
|
|
20
20
|
# @return [String, nil] evaluated value or nil if evaluation fails
|
|
21
21
|
def evaluate_expression(token, context, allow_nil: true)
|
|
22
|
-
return nil if token.nil?
|
|
23
|
-
return "" if token.empty?
|
|
24
|
-
|
|
25
22
|
# Strip outer quotes if present
|
|
26
23
|
stripped = strip_outer_quotes(token)
|
|
27
24
|
|
|
28
25
|
# If the original token was quoted, treat as literal string
|
|
29
|
-
if
|
|
26
|
+
if !token.nil? && quote_wrapped?(token)
|
|
30
27
|
return stripped if allow_nil
|
|
31
28
|
|
|
32
29
|
return stripped.empty? ? nil : stripped
|
|
@@ -40,20 +37,16 @@ module JekyllHighlightCards
|
|
|
40
37
|
result = template.render(context)
|
|
41
38
|
return result if allow_nil
|
|
42
39
|
|
|
43
|
-
return result.
|
|
44
|
-
rescue Liquid::SyntaxError, StandardError => e
|
|
45
|
-
log_debug("Failed to evaluate '#{token}' as Liquid expression: #{e.message}")
|
|
46
|
-
# Fall back to literal string
|
|
47
|
-
return token if allow_nil
|
|
40
|
+
return nil if result.empty?
|
|
48
41
|
|
|
49
|
-
|
|
42
|
+
result
|
|
43
|
+
rescue Liquid::SyntaxError, StandardError => e
|
|
44
|
+
log_debug("Failed to evaluate '#{token}' as Liquid expression: #{e.class}: #{e}")
|
|
45
|
+
token
|
|
50
46
|
end
|
|
47
|
+
else
|
|
48
|
+
token
|
|
51
49
|
end
|
|
52
|
-
|
|
53
|
-
# Return as literal string
|
|
54
|
-
return token if allow_nil
|
|
55
|
-
|
|
56
|
-
token.empty? ? nil : token
|
|
57
50
|
end
|
|
58
51
|
|
|
59
52
|
# Check if an expression looks like a Liquid variable lookup
|
|
@@ -61,7 +54,7 @@ module JekyllHighlightCards
|
|
|
61
54
|
# @param expression [String] the expression to check
|
|
62
55
|
# @return [Boolean] true if expression contains Liquid syntax
|
|
63
56
|
def variable_lookup?(expression)
|
|
64
|
-
return false if expression.nil?
|
|
57
|
+
return false if expression.nil?
|
|
65
58
|
|
|
66
59
|
expression.include?("{{") || expression.include?("{%")
|
|
67
60
|
end
|
|
@@ -71,17 +64,23 @@ module JekyllHighlightCards
|
|
|
71
64
|
# @param value [String] the string to process
|
|
72
65
|
# @return [String] string with outer quotes removed if present
|
|
73
66
|
def strip_outer_quotes(value)
|
|
74
|
-
return
|
|
75
|
-
|
|
76
|
-
# Check for matching outer quotes
|
|
77
|
-
if ((value.start_with?('"') && value.end_with?('"')) ||
|
|
78
|
-
(value.start_with?("'") && value.end_with?("'"))) && (value.length > 1)
|
|
79
|
-
return value[1..-2]
|
|
80
|
-
end
|
|
67
|
+
return nil if value.nil?
|
|
68
|
+
return value[1..-2] if quote_wrapped?(value)
|
|
81
69
|
|
|
82
70
|
value
|
|
83
71
|
end
|
|
84
72
|
|
|
73
|
+
# Whether value has matching outer quotes and more than one character
|
|
74
|
+
#
|
|
75
|
+
# @param value [String] the string to inspect
|
|
76
|
+
# @return [Boolean]
|
|
77
|
+
def quote_wrapped?(value)
|
|
78
|
+
return false unless value.length >= 2
|
|
79
|
+
|
|
80
|
+
(value.start_with?('"') && value.end_with?('"')) ||
|
|
81
|
+
(value.start_with?("'") && value.end_with?("'"))
|
|
82
|
+
end
|
|
83
|
+
|
|
85
84
|
# Log debug message
|
|
86
85
|
#
|
|
87
86
|
# @param message [String] message to log
|