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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2e7d4bb8027142ccb1dbf7e0327098460eb49fda5f10fa2619efeb9053fa1359
4
- data.tar.gz: 9971cb5b3a34a9bfbfaf06539e973df20c5951fb5bac39649378c4909a03a759
3
+ metadata.gz: 48a4ccd9a729f6d5db3c458bdf9311abe63b595b370cb6577994e7c0da53d763
4
+ data.tar.gz: 4805a92c237413bcc17ddd0c2d1166bd5c7e606a9c2287b8f6e99939d42d6639
5
5
  SHA512:
6
- metadata.gz: d1183485eba6958f3d328b693207cfbccfd0dcef3da163511332c0ad16b39c30416b8c1bb41e767b9dc8ddee5d761f92ae26bb7b56f18ada1b5d1ef8a5bbfa21
7
- data.tar.gz: 9d62f2b98e4923cdd73a75719718d92dd40b235dd0c61982e4222bc4a1f525b9593ddef3b10f3485ea56f30ad300e0be6310f8a6ccbfcc6ac50c1448092cd8d4
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: `![alt](image.jpg =300x200)`
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
- tokens = []
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(params[:archive], link_url)
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
- tokens = []
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
- tokens.each do |token|
133
- next unless token =~ /\A(\w+)=(.+)\z/
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 = Regexp.last_match(1).to_sym
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module JekyllHighlightCards
4
- VERSION = "2.1.0"
4
+ VERSION = "2.2.0"
5
5
  end
@@ -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.1.0
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