jekyll-highlight-cards 2.1.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 +12 -0
- data/README.md +36 -0
- data/lib/jekyll-highlight-cards/archive_helper.rb +50 -2
- data/lib/jekyll-highlight-cards/commands/freeze_archives.rb +178 -0
- 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/linkcard_markup.rb +83 -0
- data/lib/jekyll-highlight-cards/linkcard_tag.rb +2 -57
- data/lib/jekyll-highlight-cards/polaroid_markup.rb +81 -0
- data/lib/jekyll-highlight-cards/polaroid_tag.rb +16 -53
- data/lib/jekyll-highlight-cards/version.rb +1 -1
- data/lib/jekyll-highlight-cards.rb +6 -0
- metadata +7 -1
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,17 @@
|
|
|
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
|
+
|
|
3
15
|
## [2.1.0](https://github.com/Texarkanine/jekyll-highlight-cards/compare/v2.0.0...v2.1.0) (2026-07-19)
|
|
4
16
|
|
|
5
17
|
|
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:**
|
|
@@ -17,14 +17,17 @@ 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
32
|
archive_url = lookup_archive(url)
|
|
30
33
|
|
|
@@ -58,6 +61,51 @@ module JekyllHighlightCards
|
|
|
58
61
|
|
|
59
62
|
private
|
|
60
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
|
+
|
|
61
109
|
# Look up the latest archived snapshot for a URL via Internet Archive CDX API
|
|
62
110
|
#
|
|
63
111
|
# @param url [String] the URL to look up
|
|
@@ -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
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JekyllHighlightCards
|
|
4
|
+
module FreezeArchives
|
|
5
|
+
# Surgically insert an archive token into a located tag span.
|
|
6
|
+
#
|
|
7
|
+
# Single-line tags splice after the last non-whitespace of the markup.
|
|
8
|
+
# Multiline tags add a new line before the closer, indented like the last
|
|
9
|
+
# content line (see creative-source-scan-rewrite).
|
|
10
|
+
class ArchiveInserter
|
|
11
|
+
# Insert archive URL into +content+ at +span+
|
|
12
|
+
#
|
|
13
|
+
# @param content [String] full file contents
|
|
14
|
+
# @param span [Hash] locator span with +:tag+, +:markup+, +:range+
|
|
15
|
+
# @param archive_url [String] Wayback (or other) archive URL to freeze
|
|
16
|
+
# @return [String] updated file contents
|
|
17
|
+
def insert(content, span, archive_url)
|
|
18
|
+
range = span.fetch(:range)
|
|
19
|
+
tag_text = content[range]
|
|
20
|
+
updated = rewrite_tag(tag_text, span.fetch(:tag), archive_url)
|
|
21
|
+
content[0...range.begin] + updated + content[range.end..]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def rewrite_tag(tag_text, tag, archive_url)
|
|
27
|
+
token = archive_token(tag, archive_url)
|
|
28
|
+
match = tag_text.match(/\A(?<head>[\s\S]*?)(?<closer>-?%})\z/)
|
|
29
|
+
raise ArgumentError, "tag span missing closing %}" unless match
|
|
30
|
+
|
|
31
|
+
head = match[:head]
|
|
32
|
+
closer = match[:closer]
|
|
33
|
+
|
|
34
|
+
if head.include?("\n")
|
|
35
|
+
insert_multiline(head, closer, token)
|
|
36
|
+
else
|
|
37
|
+
insert_single_line(head, closer, token)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def insert_single_line(head, closer, token)
|
|
42
|
+
# Preserve trailing whitespace before %}
|
|
43
|
+
trailing = head[/\s*\z/] || ""
|
|
44
|
+
body = head[0...(head.length - trailing.length)]
|
|
45
|
+
"#{body} #{token}#{trailing}#{closer}"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def insert_multiline(head, closer, token)
|
|
49
|
+
lines = head.split("\n", -1)
|
|
50
|
+
# Drop trailing whitespace-only segment before %} and keep its indent for the closer
|
|
51
|
+
closer_indent = ""
|
|
52
|
+
closer_indent = lines.pop if lines.last&.strip&.empty?
|
|
53
|
+
|
|
54
|
+
last_content = lines.reverse.find { |line| !line.strip.empty? }
|
|
55
|
+
indent = last_content ? last_content[/\A[ \t]*/] : ""
|
|
56
|
+
|
|
57
|
+
"#{lines.join("\n")}\n#{indent}#{token}\n#{closer_indent}#{closer}"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def archive_token(tag, archive_url)
|
|
61
|
+
case tag.to_s
|
|
62
|
+
when "linkcard"
|
|
63
|
+
"archive:#{archive_url}"
|
|
64
|
+
when "polaroid"
|
|
65
|
+
%(archive="#{archive_url.to_s.gsub('"', '\\"')}")
|
|
66
|
+
else
|
|
67
|
+
raise ArgumentError, "unsupported tag: #{tag}"
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JekyllHighlightCards
|
|
4
|
+
module FreezeArchives
|
|
5
|
+
# Classify `{% linkcard %}` / `{% polaroid %}` markup for freeze-archives.
|
|
6
|
+
#
|
|
7
|
+
# A freeze candidate is a tag that has a literal archiveable target URL and
|
|
8
|
+
# does not already encode an archive (`archive:` / `archive=` / `none`).
|
|
9
|
+
# Liquid-dynamic targets are skipped (build-time fallback remains).
|
|
10
|
+
class MarkupAnalyzer
|
|
11
|
+
include ArchiveHelper
|
|
12
|
+
include ExpressionEvaluator
|
|
13
|
+
|
|
14
|
+
# @param site [Jekyll::Site, nil] site for +highlight_cards.noarchive+
|
|
15
|
+
def initialize(site: nil)
|
|
16
|
+
@site = site
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Analyze tag markup for freeze eligibility
|
|
20
|
+
#
|
|
21
|
+
# @param tag [String] +"linkcard"+ or +"polaroid"+
|
|
22
|
+
# @param markup [String] contents between the tag name and +%}+
|
|
23
|
+
# @return [Hash, nil] +{ target_url: String }+ when freezable, else +nil+
|
|
24
|
+
def analyze(tag, markup)
|
|
25
|
+
case tag.to_s
|
|
26
|
+
when "linkcard"
|
|
27
|
+
analyze_linkcard(markup)
|
|
28
|
+
when "polaroid"
|
|
29
|
+
analyze_polaroid(markup)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def analyze_linkcard(markup)
|
|
36
|
+
parsed = LinkcardMarkup.split(markup)
|
|
37
|
+
return nil if parsed.key?(:archive)
|
|
38
|
+
|
|
39
|
+
candidate_for(strip_outer_quotes(parsed[:url]))
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def analyze_polaroid(markup)
|
|
43
|
+
parsed = PolaroidMarkup.parse(markup)
|
|
44
|
+
return nil if parsed.key?(:archive)
|
|
45
|
+
return nil unless parsed.key?(:link)
|
|
46
|
+
|
|
47
|
+
candidate_for(strip_outer_quotes(parsed[:link]))
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def candidate_for(target)
|
|
51
|
+
return nil if target.to_s.empty?
|
|
52
|
+
return nil if variable_lookup?(target)
|
|
53
|
+
return nil unless archiveable_url?(target, site: @site)
|
|
54
|
+
|
|
55
|
+
{ target_url: target }
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JekyllHighlightCards
|
|
4
|
+
module FreezeArchives
|
|
5
|
+
# Locate `{% linkcard %}` / `{% polaroid %}` spans in source text.
|
|
6
|
+
#
|
|
7
|
+
# Supports multiline tags. Returns structural spans for analysis/insert —
|
|
8
|
+
# does not decide freeze eligibility.
|
|
9
|
+
class TagLocator
|
|
10
|
+
# Optional +- after +{ %+ / before +%}+ is Liquid whitespace control —
|
|
11
|
+
# keep the trailing dash out of +:markup+ (leading dash stays in the opener).
|
|
12
|
+
TAG_PATTERN = /{%-?\s*(linkcard|polaroid)\s+([\s\S]*?)-?%}/
|
|
13
|
+
|
|
14
|
+
# Find highlight-card tag spans in +text+
|
|
15
|
+
#
|
|
16
|
+
# @param text [String] file contents
|
|
17
|
+
# @return [Array<Hash>] each hash has +:tag+, +:markup+, +:range+ (exclusive end)
|
|
18
|
+
def locate(text)
|
|
19
|
+
spans = []
|
|
20
|
+
text.to_s.scan(TAG_PATTERN) do
|
|
21
|
+
match = Regexp.last_match
|
|
22
|
+
spans << {
|
|
23
|
+
tag: match[1],
|
|
24
|
+
markup: match[2],
|
|
25
|
+
range: match.begin(0)...match.end(0)
|
|
26
|
+
}
|
|
27
|
+
end
|
|
28
|
+
spans
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JekyllHighlightCards
|
|
4
|
+
# Shared structural parser for `{% linkcard %}` markup.
|
|
5
|
+
#
|
|
6
|
+
# Used by {LinkcardTag} at render time and by freeze-archives analysis
|
|
7
|
+
# without Liquid evaluation. Tokens are returned as written in source.
|
|
8
|
+
module LinkcardMarkup
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
# Parse linkcard markup into URL, title, and archive components
|
|
12
|
+
#
|
|
13
|
+
# @param markup [String] the tag markup (contents between tag name and `%}`)
|
|
14
|
+
# @return [Hash] keys +:url+, optional +:title+, optional +:archive+ (unevaluated)
|
|
15
|
+
def split(markup)
|
|
16
|
+
tokens = tokenize(markup)
|
|
17
|
+
|
|
18
|
+
result = {}
|
|
19
|
+
result[:url] = tokens.shift
|
|
20
|
+
|
|
21
|
+
title_tokens = []
|
|
22
|
+
tokens.each do |token|
|
|
23
|
+
if token.start_with?("archive:")
|
|
24
|
+
result[:archive] = token.delete_prefix("archive:")
|
|
25
|
+
else
|
|
26
|
+
title_tokens << token
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
result[:title] = title_tokens.join(" ") unless title_tokens.empty?
|
|
30
|
+
|
|
31
|
+
result
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Tokenize markup respecting quotes and Liquid brace depth
|
|
35
|
+
#
|
|
36
|
+
# @param markup [String] the tag markup
|
|
37
|
+
# @return [Array<String>] non-empty tokens
|
|
38
|
+
def tokenize(markup)
|
|
39
|
+
tokens = []
|
|
40
|
+
current = ""
|
|
41
|
+
in_quotes = false
|
|
42
|
+
quote_char = nil
|
|
43
|
+
in_liquid = 0
|
|
44
|
+
escaped = false
|
|
45
|
+
|
|
46
|
+
"#{markup} ".each_char do |char|
|
|
47
|
+
if escaped
|
|
48
|
+
current += char
|
|
49
|
+
escaped = false
|
|
50
|
+
next
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
if char == "\\" && in_quotes
|
|
54
|
+
escaped = true
|
|
55
|
+
next
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
if char == "{" && !in_quotes
|
|
59
|
+
in_liquid += 1
|
|
60
|
+
current += char
|
|
61
|
+
elsif char == "}" && in_liquid.positive? && !in_quotes
|
|
62
|
+
in_liquid -= 1
|
|
63
|
+
current += char
|
|
64
|
+
elsif char == '"' && !in_quotes
|
|
65
|
+
in_quotes = true
|
|
66
|
+
quote_char = char
|
|
67
|
+
current += char
|
|
68
|
+
elsif char == quote_char
|
|
69
|
+
in_quotes = false
|
|
70
|
+
current += char
|
|
71
|
+
quote_char = nil
|
|
72
|
+
elsif char.match?(/\s/) && !in_quotes && in_liquid.zero?
|
|
73
|
+
tokens << current
|
|
74
|
+
current = ""
|
|
75
|
+
else
|
|
76
|
+
current += char
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
tokens.reject!(&:empty?)
|
|
80
|
+
tokens
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -62,62 +62,7 @@ module JekyllHighlightCards
|
|
|
62
62
|
# @param markup [String] the tag markup
|
|
63
63
|
# @return [Hash] parsed components
|
|
64
64
|
def split_markup(markup)
|
|
65
|
-
|
|
66
|
-
current = ""
|
|
67
|
-
in_quotes = false
|
|
68
|
-
quote_char = nil
|
|
69
|
-
in_liquid = 0
|
|
70
|
-
escaped = false
|
|
71
|
-
|
|
72
|
-
"#{markup} ".each_char do |char|
|
|
73
|
-
if escaped
|
|
74
|
-
current += char
|
|
75
|
-
escaped = false
|
|
76
|
-
next
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
if char == "\\" && in_quotes
|
|
80
|
-
escaped = true
|
|
81
|
-
next
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
if char == "{" && !in_quotes
|
|
85
|
-
in_liquid += 1
|
|
86
|
-
current += char
|
|
87
|
-
elsif char == "}" && in_liquid.positive?
|
|
88
|
-
in_liquid -= 1
|
|
89
|
-
current += char
|
|
90
|
-
elsif char == '"' && !in_quotes
|
|
91
|
-
in_quotes = true
|
|
92
|
-
quote_char = char
|
|
93
|
-
current += char
|
|
94
|
-
elsif char == quote_char
|
|
95
|
-
in_quotes = false
|
|
96
|
-
current += char
|
|
97
|
-
quote_char = nil
|
|
98
|
-
elsif char.match?(/\s/) && !in_quotes && in_liquid.zero?
|
|
99
|
-
tokens << current
|
|
100
|
-
current = ""
|
|
101
|
-
else
|
|
102
|
-
current += char
|
|
103
|
-
end
|
|
104
|
-
end
|
|
105
|
-
tokens.reject!(&:empty?)
|
|
106
|
-
|
|
107
|
-
result = {}
|
|
108
|
-
result[:url] = tokens.shift
|
|
109
|
-
|
|
110
|
-
title_tokens = []
|
|
111
|
-
tokens.each do |token|
|
|
112
|
-
if token.start_with?("archive:")
|
|
113
|
-
result[:archive] = token.delete_prefix("archive:")
|
|
114
|
-
else
|
|
115
|
-
title_tokens << token
|
|
116
|
-
end
|
|
117
|
-
end
|
|
118
|
-
result[:title] = title_tokens.join(" ") unless title_tokens.empty?
|
|
119
|
-
|
|
120
|
-
result
|
|
65
|
+
LinkcardMarkup.split(markup)
|
|
121
66
|
end
|
|
122
67
|
|
|
123
68
|
# Resolve URL from token (may be Liquid expression or literal)
|
|
@@ -151,7 +96,7 @@ module JekyllHighlightCards
|
|
|
151
96
|
# Check for explicit archive URL
|
|
152
97
|
return evaluate_expression(source, context) if source && !source.empty?
|
|
153
98
|
|
|
154
|
-
archive_enabled? && archive_url_for(url)
|
|
99
|
+
archive_enabled? && archive_url_for(url, site: context.registers[:site])
|
|
155
100
|
end
|
|
156
101
|
|
|
157
102
|
# Build template variables hash for rendering
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JekyllHighlightCards
|
|
4
|
+
# Shared structural parser for `{% polaroid %}` markup.
|
|
5
|
+
#
|
|
6
|
+
# Used by {PolaroidTag} at render time (with Liquid evaluation applied by the
|
|
7
|
+
# tag) and by freeze-archives analysis on unevaluated tokens.
|
|
8
|
+
module PolaroidMarkup
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
# Parse polaroid markup into image URL and named parameters (unevaluated)
|
|
12
|
+
#
|
|
13
|
+
# @param markup [String] the tag markup (contents between tag name and `%}`)
|
|
14
|
+
# @return [Hash] +:image_url+ plus any +key=value+ params as symbols (raw tokens)
|
|
15
|
+
def parse(markup)
|
|
16
|
+
tokens = tokenize(markup)
|
|
17
|
+
|
|
18
|
+
image_url_token = tokens.shift
|
|
19
|
+
result = { image_url: image_url_token }
|
|
20
|
+
tokens.each do |token|
|
|
21
|
+
next unless token =~ /\A(\w+)=(.+)\z/
|
|
22
|
+
|
|
23
|
+
key = Regexp.last_match(1).to_sym
|
|
24
|
+
value_token = Regexp.last_match(2)
|
|
25
|
+
result[key] = value_token
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
result
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Tokenize markup respecting quotes (single or double) and Liquid brace depth
|
|
32
|
+
#
|
|
33
|
+
# @param markup [String] the tag markup
|
|
34
|
+
# @return [Array<String>] tokens (may include empty strings from leading whitespace)
|
|
35
|
+
def tokenize(markup)
|
|
36
|
+
tokens = []
|
|
37
|
+
current = ""
|
|
38
|
+
in_quotes = false
|
|
39
|
+
quote_char = nil
|
|
40
|
+
in_liquid = 0
|
|
41
|
+
escaped = false
|
|
42
|
+
|
|
43
|
+
# Trailing space flushes the final token through the whitespace branch
|
|
44
|
+
"#{markup} ".each_char do |char|
|
|
45
|
+
if escaped
|
|
46
|
+
current += char
|
|
47
|
+
escaped = false
|
|
48
|
+
next
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
if char == "\\" && in_quotes
|
|
52
|
+
escaped = true
|
|
53
|
+
next
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
if char == "{" && !in_quotes
|
|
57
|
+
in_liquid += 1
|
|
58
|
+
current += char
|
|
59
|
+
elsif char == "}" && in_liquid.positive? && !in_quotes
|
|
60
|
+
in_liquid -= 1
|
|
61
|
+
current += char
|
|
62
|
+
elsif ['"', "'"].include?(char) && !in_quotes
|
|
63
|
+
in_quotes = true
|
|
64
|
+
quote_char = char
|
|
65
|
+
current += char
|
|
66
|
+
elsif char == quote_char
|
|
67
|
+
in_quotes = false
|
|
68
|
+
current += char
|
|
69
|
+
quote_char = nil
|
|
70
|
+
elsif char.match?(/\s/) && !in_quotes && in_liquid.zero?
|
|
71
|
+
tokens << current
|
|
72
|
+
current = ""
|
|
73
|
+
else
|
|
74
|
+
current += char
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
tokens
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -58,7 +58,12 @@ module JekyllHighlightCards
|
|
|
58
58
|
else
|
|
59
59
|
params.fetch(:image_link)
|
|
60
60
|
end
|
|
61
|
-
archive_url = resolve_archive(
|
|
61
|
+
archive_url = resolve_archive(
|
|
62
|
+
params[:archive],
|
|
63
|
+
link_url,
|
|
64
|
+
site: context.registers[:site],
|
|
65
|
+
explicit_link: explicit_link
|
|
66
|
+
)
|
|
62
67
|
|
|
63
68
|
variables = build_template_variables(
|
|
64
69
|
params.fetch(:image_url),
|
|
@@ -83,58 +88,13 @@ module JekyllHighlightCards
|
|
|
83
88
|
# @param context [Liquid::Context] the Liquid context
|
|
84
89
|
# @return [Hash] parsed parameters
|
|
85
90
|
def parse_markup(markup, context)
|
|
86
|
-
|
|
87
|
-
current = ""
|
|
88
|
-
in_quotes = false
|
|
89
|
-
quote_char = nil
|
|
90
|
-
in_liquid = 0
|
|
91
|
-
escaped = false
|
|
92
|
-
|
|
93
|
-
# Trailing space flushes the final token through the whitespace branch
|
|
94
|
-
"#{markup} ".each_char do |char|
|
|
95
|
-
if escaped
|
|
96
|
-
current += char
|
|
97
|
-
escaped = false
|
|
98
|
-
next
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
if char == "\\" && in_quotes
|
|
102
|
-
escaped = true
|
|
103
|
-
next
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
if char == "{" && !in_quotes
|
|
107
|
-
in_liquid += 1
|
|
108
|
-
current += char
|
|
109
|
-
elsif char == "}" && in_liquid.positive?
|
|
110
|
-
in_liquid -= 1
|
|
111
|
-
current += char
|
|
112
|
-
elsif ['"', "'"].include?(char) && !in_quotes
|
|
113
|
-
in_quotes = true
|
|
114
|
-
quote_char = char
|
|
115
|
-
current += char
|
|
116
|
-
elsif char == quote_char
|
|
117
|
-
in_quotes = false
|
|
118
|
-
current += char
|
|
119
|
-
quote_char = nil
|
|
120
|
-
elsif char.match?(/\s/) && !in_quotes && in_liquid.zero?
|
|
121
|
-
tokens << current
|
|
122
|
-
current = ""
|
|
123
|
-
else
|
|
124
|
-
current += char
|
|
125
|
-
end
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
image_url_token = tokens.shift
|
|
129
|
-
image_url = evaluate_expression(image_url_token, context)
|
|
91
|
+
parsed = PolaroidMarkup.parse(markup)
|
|
130
92
|
|
|
131
|
-
result = { image_url: image_url }
|
|
132
|
-
|
|
133
|
-
next
|
|
93
|
+
result = { image_url: evaluate_expression(parsed[:image_url], context) }
|
|
94
|
+
parsed.each do |key, value|
|
|
95
|
+
next if key == :image_url
|
|
134
96
|
|
|
135
|
-
key =
|
|
136
|
-
value_token = Regexp.last_match(2)
|
|
137
|
-
result[key] = evaluate_expression(value_token, context)
|
|
97
|
+
result[key] = evaluate_expression(value, context)
|
|
138
98
|
end
|
|
139
99
|
|
|
140
100
|
result
|
|
@@ -145,12 +105,15 @@ module JekyllHighlightCards
|
|
|
145
105
|
#
|
|
146
106
|
# @param source [String, nil] the archive source
|
|
147
107
|
# @param url [String] the target URL to archive
|
|
108
|
+
# @param site [Jekyll::Site, nil] site for +highlight_cards.noarchive+
|
|
109
|
+
# @param explicit_link [Boolean] whether +link=+ was provided (self-link is not an archive target)
|
|
148
110
|
# @return [String, nil] resolved archive URL
|
|
149
|
-
def resolve_archive(source, url)
|
|
111
|
+
def resolve_archive(source, url, site: nil, explicit_link: true)
|
|
150
112
|
return nil if source && source.downcase == "none"
|
|
151
113
|
return source unless source.to_s.empty?
|
|
114
|
+
return nil unless explicit_link
|
|
152
115
|
|
|
153
|
-
archive_enabled? && archive_url_for(url)
|
|
116
|
+
archive_enabled? && archive_url_for(url, site: site)
|
|
154
117
|
end
|
|
155
118
|
|
|
156
119
|
# Build template variables hash for rendering
|
|
@@ -12,11 +12,17 @@ require_relative "jekyll-highlight-cards/dimension_parser"
|
|
|
12
12
|
require_relative "jekyll-highlight-cards/expression_evaluator"
|
|
13
13
|
require_relative "jekyll-highlight-cards/archive_helper"
|
|
14
14
|
require_relative "jekyll-highlight-cards/template_renderer"
|
|
15
|
+
require_relative "jekyll-highlight-cards/linkcard_markup"
|
|
16
|
+
require_relative "jekyll-highlight-cards/polaroid_markup"
|
|
15
17
|
|
|
16
18
|
# Require tags and hooks
|
|
17
19
|
require_relative "jekyll-highlight-cards/linkcard_tag"
|
|
18
20
|
require_relative "jekyll-highlight-cards/polaroid_tag"
|
|
19
21
|
require_relative "jekyll-highlight-cards/image_sizing_hooks"
|
|
22
|
+
require_relative "jekyll-highlight-cards/freeze_archives/markup_analyzer"
|
|
23
|
+
require_relative "jekyll-highlight-cards/freeze_archives/tag_locator"
|
|
24
|
+
require_relative "jekyll-highlight-cards/freeze_archives/archive_inserter"
|
|
25
|
+
require_relative "jekyll-highlight-cards/commands/freeze_archives"
|
|
20
26
|
|
|
21
27
|
# jekyll-highlight-cards: Styled card components for Jekyll
|
|
22
28
|
#
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: jekyll-highlight-cards
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Texarkanine
|
|
@@ -223,10 +223,16 @@ files:
|
|
|
223
223
|
- jekyll-highlight-cards.gemspec
|
|
224
224
|
- lib/jekyll-highlight-cards.rb
|
|
225
225
|
- lib/jekyll-highlight-cards/archive_helper.rb
|
|
226
|
+
- lib/jekyll-highlight-cards/commands/freeze_archives.rb
|
|
226
227
|
- lib/jekyll-highlight-cards/dimension_parser.rb
|
|
227
228
|
- lib/jekyll-highlight-cards/expression_evaluator.rb
|
|
229
|
+
- lib/jekyll-highlight-cards/freeze_archives/archive_inserter.rb
|
|
230
|
+
- lib/jekyll-highlight-cards/freeze_archives/markup_analyzer.rb
|
|
231
|
+
- lib/jekyll-highlight-cards/freeze_archives/tag_locator.rb
|
|
228
232
|
- lib/jekyll-highlight-cards/image_sizing_hooks.rb
|
|
233
|
+
- lib/jekyll-highlight-cards/linkcard_markup.rb
|
|
229
234
|
- lib/jekyll-highlight-cards/linkcard_tag.rb
|
|
235
|
+
- lib/jekyll-highlight-cards/polaroid_markup.rb
|
|
230
236
|
- lib/jekyll-highlight-cards/polaroid_tag.rb
|
|
231
237
|
- lib/jekyll-highlight-cards/template_renderer.rb
|
|
232
238
|
- lib/jekyll-highlight-cards/version.rb
|