jekyll-highlight-cards 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.cursorignore +1 -0
- data/.github/workflows/ci.yaml +21 -0
- data/.github/workflows/release-please.yaml +69 -0
- data/.github/workflows/update-gemfile-lock.yaml +52 -0
- data/.gitignore +59 -0
- data/.release-please-manifest.json +3 -0
- data/.rspec +5 -0
- data/.rubocop.yml +64 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +54 -0
- data/CONTRIBUTING.md +219 -0
- data/Gemfile +5 -0
- data/Gemfile.lock +196 -0
- data/LICENSE +661 -0
- data/README.md +228 -0
- data/_includes/highlight-cards/linkcard.html +13 -0
- data/_includes/highlight-cards/polaroid.html +22 -0
- data/_sass/_highlight-cards.scss +92 -0
- data/docs/linkcard-example.jpg +0 -0
- data/docs/polaroid-example.jpg +0 -0
- data/docs/polaroid-sidebyside-example.jpg +0 -0
- data/docs/polaroid-stacked-example.jpg +0 -0
- data/jekyll-highlight-cards.gemspec +47 -0
- data/lib/jekyll-highlight-cards/archive_helper.rb +151 -0
- data/lib/jekyll-highlight-cards/dimension_parser.rb +62 -0
- data/lib/jekyll-highlight-cards/expression_evaluator.rb +113 -0
- data/lib/jekyll-highlight-cards/image_sizing_hooks.rb +188 -0
- data/lib/jekyll-highlight-cards/linkcard_tag.rb +211 -0
- data/lib/jekyll-highlight-cards/polaroid_tag.rb +223 -0
- data/lib/jekyll-highlight-cards/template_renderer.rb +113 -0
- data/lib/jekyll-highlight-cards/version.rb +5 -0
- data/lib/jekyll-highlight-cards.rb +57 -0
- data/release-please-config.json +12 -0
- metadata +234 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JekyllHighlightCards
|
|
4
|
+
# Evaluate Liquid expressions and handle string values
|
|
5
|
+
#
|
|
6
|
+
# Provides utilities for evaluating Liquid variables (e.g., `{{ page.title }}`)
|
|
7
|
+
# and processing string values with quote stripping and fallback behavior.
|
|
8
|
+
#
|
|
9
|
+
# @example Evaluate a Liquid variable
|
|
10
|
+
# result = evaluate_expression("{{ page.title }}", context)
|
|
11
|
+
#
|
|
12
|
+
# @example Evaluate a quoted string
|
|
13
|
+
# result = evaluate_expression('"My Title"', context) #=> "My Title"
|
|
14
|
+
module ExpressionEvaluator
|
|
15
|
+
# Evaluate a token as a Liquid expression or literal string
|
|
16
|
+
#
|
|
17
|
+
# @param token [String] the token to evaluate
|
|
18
|
+
# @param context [Liquid::Context] the Liquid context for variable resolution
|
|
19
|
+
# @param allow_nil [Boolean] whether to allow nil results
|
|
20
|
+
# @return [String, nil] evaluated value or nil if evaluation fails
|
|
21
|
+
def evaluate_expression(token, context, allow_nil: true)
|
|
22
|
+
return nil if token.nil?
|
|
23
|
+
return "" if token.empty?
|
|
24
|
+
|
|
25
|
+
# Strip outer quotes if present
|
|
26
|
+
stripped = strip_outer_quotes(token)
|
|
27
|
+
|
|
28
|
+
# If the original token was quoted, treat as literal string
|
|
29
|
+
if stripped != token
|
|
30
|
+
return stripped if allow_nil
|
|
31
|
+
|
|
32
|
+
return stripped.empty? ? nil : stripped
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Try to evaluate as Liquid expression
|
|
36
|
+
if variable_lookup?(token)
|
|
37
|
+
begin
|
|
38
|
+
# Parse and evaluate the Liquid expression
|
|
39
|
+
template = Liquid::Template.parse(token)
|
|
40
|
+
result = template.render(context)
|
|
41
|
+
return result if allow_nil
|
|
42
|
+
|
|
43
|
+
return result.to_s.empty? ? nil : 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
|
|
48
|
+
|
|
49
|
+
return token.empty? ? nil : token
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Return as literal string
|
|
54
|
+
return token if allow_nil
|
|
55
|
+
|
|
56
|
+
token.empty? ? nil : token
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Check if an expression looks like a Liquid variable lookup
|
|
60
|
+
#
|
|
61
|
+
# @param expression [String] the expression to check
|
|
62
|
+
# @return [Boolean] true if expression contains Liquid syntax
|
|
63
|
+
def variable_lookup?(expression)
|
|
64
|
+
return false if expression.nil? || expression.empty?
|
|
65
|
+
|
|
66
|
+
expression.include?("{{") || expression.include?("{%")
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Strip outer quotes from a string (both single and double quotes)
|
|
70
|
+
#
|
|
71
|
+
# @param value [String] the string to process
|
|
72
|
+
# @return [String] string with outer quotes removed if present
|
|
73
|
+
def strip_outer_quotes(value)
|
|
74
|
+
return value if value.nil? || value.empty?
|
|
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
|
|
81
|
+
|
|
82
|
+
value
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Log debug message
|
|
86
|
+
#
|
|
87
|
+
# @param message [String] message to log
|
|
88
|
+
def log_debug(message)
|
|
89
|
+
Jekyll.logger.debug("HighlightCards:", message)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Log info message
|
|
93
|
+
#
|
|
94
|
+
# @param message [String] message to log
|
|
95
|
+
def log_info(message)
|
|
96
|
+
Jekyll.logger.info("HighlightCards:", message)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Log warning message
|
|
100
|
+
#
|
|
101
|
+
# @param message [String] message to log
|
|
102
|
+
def log_warn(message)
|
|
103
|
+
Jekyll.logger.warn("HighlightCards:", message)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Log error message
|
|
107
|
+
#
|
|
108
|
+
# @param message [String] message to log
|
|
109
|
+
def log_error(message)
|
|
110
|
+
Jekyll.logger.error("HighlightCards:", message)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JekyllHighlightCards
|
|
4
|
+
# Jekyll hooks for Markdown image sizing syntax
|
|
5
|
+
#
|
|
6
|
+
# Extends standard Markdown image syntax with dimension specifiers:
|
|
7
|
+
# ``
|
|
8
|
+
#
|
|
9
|
+
# Automatically wraps sized images in links to themselves and applies
|
|
10
|
+
# width/height attributes. Skips images in code blocks.
|
|
11
|
+
#
|
|
12
|
+
# @example Basic sizing
|
|
13
|
+
# 
|
|
14
|
+
#
|
|
15
|
+
# @example Width only
|
|
16
|
+
# 
|
|
17
|
+
#
|
|
18
|
+
# @example Height only
|
|
19
|
+
# 
|
|
20
|
+
#
|
|
21
|
+
# @note Images inside code fences and inline code are not processed
|
|
22
|
+
# @note Sized images are automatically wrapped in <a> tags (if not already)
|
|
23
|
+
module ImageSizingHooks
|
|
24
|
+
extend DimensionParser
|
|
25
|
+
|
|
26
|
+
# Process document content before rendering
|
|
27
|
+
# Converts  to <!-- IMG_SIZE:W:H -->
|
|
28
|
+
#
|
|
29
|
+
# @param document [Jekyll::Document] the document being processed
|
|
30
|
+
def self.process_pre_render(document)
|
|
31
|
+
content = document.content
|
|
32
|
+
return unless content
|
|
33
|
+
|
|
34
|
+
lines = content.lines
|
|
35
|
+
result = []
|
|
36
|
+
|
|
37
|
+
lines.each_with_index do |line, idx|
|
|
38
|
+
# Skip lines in code fences
|
|
39
|
+
if in_code_fence?(lines, idx)
|
|
40
|
+
result << line
|
|
41
|
+
next
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Process the line to convert sized images
|
|
45
|
+
processed_line = line.dup
|
|
46
|
+
|
|
47
|
+
# Match  pattern
|
|
48
|
+
# Use gsub with block to check each match
|
|
49
|
+
processed_line.gsub!(/!\[([^\]]*)\]\(([^)]+)\s+=([^)]+)\)/) do |match|
|
|
50
|
+
match_start = Regexp.last_match.begin(0)
|
|
51
|
+
|
|
52
|
+
# Skip if inside inline code
|
|
53
|
+
if in_inline_code?(line, match_start)
|
|
54
|
+
match
|
|
55
|
+
else
|
|
56
|
+
alt = Regexp.last_match(1)
|
|
57
|
+
src = Regexp.last_match(2).strip
|
|
58
|
+
size = Regexp.last_match(3).strip
|
|
59
|
+
|
|
60
|
+
# Parse dimensions using DimensionParser
|
|
61
|
+
width, height = parse_dimensions(size)
|
|
62
|
+
|
|
63
|
+
# Build marker comment
|
|
64
|
+
"<!-- IMG_SIZE:#{width}:#{height} -->"
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
result << processed_line
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
document.content = result.join
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Process document content after rendering
|
|
75
|
+
# Applies width/height attributes and auto-links sized images
|
|
76
|
+
#
|
|
77
|
+
# @param document [Jekyll::Document] the document being processed
|
|
78
|
+
def self.process_post_render(document)
|
|
79
|
+
output = document.output
|
|
80
|
+
return unless output
|
|
81
|
+
|
|
82
|
+
# Duplicate to avoid frozen string errors
|
|
83
|
+
output = output.dup
|
|
84
|
+
|
|
85
|
+
# Match <img><!-- IMG_SIZE:W:H --> patterns
|
|
86
|
+
output.gsub!(/(<img\s+[^>]*>)\s*<!--\s*IMG_SIZE:([^:]*):([^:]*)\s*-->/) do
|
|
87
|
+
img_tag = Regexp.last_match(1)
|
|
88
|
+
width = Regexp.last_match(2).to_s.strip
|
|
89
|
+
height = Regexp.last_match(3).to_s.strip
|
|
90
|
+
|
|
91
|
+
# Build attributes to add
|
|
92
|
+
attrs = []
|
|
93
|
+
attrs << %(width="#{width}") unless width.to_s.empty?
|
|
94
|
+
attrs << %(height="#{height}") unless height.to_s.empty?
|
|
95
|
+
|
|
96
|
+
# Add attributes to img tag
|
|
97
|
+
modified_img = if attrs.any?
|
|
98
|
+
img_tag.sub("<img", "<img #{attrs.join(" ")}")
|
|
99
|
+
else
|
|
100
|
+
img_tag
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Extract src for auto-linking
|
|
104
|
+
src = img_tag[/src=["']([^"']+)["']/, 1]
|
|
105
|
+
|
|
106
|
+
# Skip auto-linking if src is missing or malformed
|
|
107
|
+
next modified_img if src.nil? || src.empty?
|
|
108
|
+
|
|
109
|
+
# Check if image is already in a link (look back in output)
|
|
110
|
+
# Simple heuristic: check if there's an <a> tag before this image without a closing </a>
|
|
111
|
+
img_position = Regexp.last_match.begin(0)
|
|
112
|
+
prefix = output[0...img_position]
|
|
113
|
+
|
|
114
|
+
# Count <a> and </a> tags before this image
|
|
115
|
+
open_count = prefix.scan(/<a\s+/).length
|
|
116
|
+
close_count = prefix.scan(%r{</a>}).length
|
|
117
|
+
already_linked = (open_count > close_count)
|
|
118
|
+
|
|
119
|
+
# Auto-link if not already linked
|
|
120
|
+
if already_linked
|
|
121
|
+
modified_img
|
|
122
|
+
else
|
|
123
|
+
%(<a href="#{CGI.escapeHTML(src)}">#{modified_img}</a>)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
document.output = output
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Check if a line is inside a code fence
|
|
131
|
+
#
|
|
132
|
+
# @param lines [Array<String>] all lines in the document
|
|
133
|
+
# @param line_idx [Integer] the current line index
|
|
134
|
+
# @return [Boolean] true if inside code fence
|
|
135
|
+
def self.in_code_fence?(lines, line_idx)
|
|
136
|
+
# Check for fenced code blocks (backticks or tildes)
|
|
137
|
+
fence_count = 0
|
|
138
|
+
(0...line_idx).each do |i|
|
|
139
|
+
# Match both backtick and tilde fences
|
|
140
|
+
fence_count += 1 if lines[i] =~ /^(`{3,}|~{3,})/
|
|
141
|
+
end
|
|
142
|
+
# Odd count means we're inside a fence
|
|
143
|
+
return true if fence_count.odd?
|
|
144
|
+
|
|
145
|
+
# Check for indented code blocks
|
|
146
|
+
# A line is in an indented code block if there's a contiguous run
|
|
147
|
+
# of indented lines (4+ spaces or tab) leading up to it
|
|
148
|
+
return false if line_idx.zero?
|
|
149
|
+
|
|
150
|
+
# Check if current line and previous lines are indented
|
|
151
|
+
idx = line_idx
|
|
152
|
+
while idx > 0
|
|
153
|
+
line = lines[idx]
|
|
154
|
+
# If line starts with 4+ spaces or tab, it's indented code
|
|
155
|
+
break unless line =~ /^( |\t)/
|
|
156
|
+
|
|
157
|
+
idx -= 1
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# If we found a contiguous run of indented lines reaching current line
|
|
161
|
+
idx < line_idx
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Check if text position is inside inline code
|
|
165
|
+
#
|
|
166
|
+
# @param line [String] the line of text
|
|
167
|
+
# @param position [Integer] the position in the line
|
|
168
|
+
# @return [Boolean] true if inside inline code
|
|
169
|
+
def self.in_inline_code?(line, position)
|
|
170
|
+
# Count backticks before the position
|
|
171
|
+
backtick_count = 0
|
|
172
|
+
line[0...position].each_char do |char|
|
|
173
|
+
backtick_count += 1 if char == "`"
|
|
174
|
+
end
|
|
175
|
+
# Odd count means we're inside inline code
|
|
176
|
+
backtick_count.odd?
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Register Jekyll hooks
|
|
182
|
+
Jekyll::Hooks.register :documents, :pre_render do |document|
|
|
183
|
+
JekyllHighlightCards::ImageSizingHooks.process_pre_render(document)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
Jekyll::Hooks.register :documents, :post_render do |document|
|
|
187
|
+
JekyllHighlightCards::ImageSizingHooks.process_post_render(document)
|
|
188
|
+
end
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JekyllHighlightCards
|
|
4
|
+
# Liquid tag for creating styled link card components
|
|
5
|
+
#
|
|
6
|
+
# Syntax:
|
|
7
|
+
# {% linkcard URL [TITLE] [archive:ARCHIVE_URL] %}
|
|
8
|
+
#
|
|
9
|
+
# Parameters:
|
|
10
|
+
# - URL (required): The URL to link to (can be Liquid expression)
|
|
11
|
+
# - TITLE (optional): Title text to display (can be Liquid expression)
|
|
12
|
+
# - archive:URL (optional): Explicit archive URL or "none" to opt out
|
|
13
|
+
#
|
|
14
|
+
# Examples:
|
|
15
|
+
# {% linkcard https://example.com %}
|
|
16
|
+
# {% linkcard https://example.com My Link Title %}
|
|
17
|
+
# {% linkcard {{ page.url }} {{ page.title }} %}
|
|
18
|
+
# {% linkcard https://example.com Title archive:none %}
|
|
19
|
+
class LinkcardTag < Liquid::Tag
|
|
20
|
+
include ArchiveHelper
|
|
21
|
+
include ExpressionEvaluator
|
|
22
|
+
include TemplateRenderer
|
|
23
|
+
|
|
24
|
+
# Initialize the tag
|
|
25
|
+
#
|
|
26
|
+
# @param tag_name [String] the name of the tag
|
|
27
|
+
# @param markup [String] the tag markup containing parameters
|
|
28
|
+
# @param tokens [Array] parse tokens (unused)
|
|
29
|
+
def initialize(tag_name, markup, tokens)
|
|
30
|
+
super
|
|
31
|
+
@markup = markup.strip
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Render the linkcard tag
|
|
35
|
+
#
|
|
36
|
+
# @param context [Liquid::Context] the Liquid rendering context
|
|
37
|
+
# @return [String] rendered HTML
|
|
38
|
+
def render(context)
|
|
39
|
+
# Parse markup
|
|
40
|
+
parsed = split_markup(@markup)
|
|
41
|
+
|
|
42
|
+
# Resolve URL (required)
|
|
43
|
+
url = resolve_url(parsed[:url], context)
|
|
44
|
+
raise ArgumentError, "linkcard tag requires a URL" if url.nil? || url.empty?
|
|
45
|
+
|
|
46
|
+
# Resolve title (optional)
|
|
47
|
+
title = resolve_title(parsed[:title], context)
|
|
48
|
+
|
|
49
|
+
# Resolve archive (optional, may auto-lookup)
|
|
50
|
+
archive_url = resolve_archive(parsed[:archive], context, url)
|
|
51
|
+
|
|
52
|
+
# Build template variables
|
|
53
|
+
variables = build_template_variables(url, title, archive_url)
|
|
54
|
+
|
|
55
|
+
# Get site from context
|
|
56
|
+
site = context.registers[:site]
|
|
57
|
+
|
|
58
|
+
# Render template
|
|
59
|
+
render_template(site, "linkcard", variables)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
# Parse the tag markup into URL, title, and archive components
|
|
65
|
+
#
|
|
66
|
+
# @param markup [String] the tag markup
|
|
67
|
+
# @return [Hash] parsed components
|
|
68
|
+
def split_markup(markup)
|
|
69
|
+
# Split by whitespace, keeping quoted strings and Liquid expressions together
|
|
70
|
+
# Handles escaped quotes (\") and backslashes (\\) within quoted strings
|
|
71
|
+
tokens = []
|
|
72
|
+
current = ""
|
|
73
|
+
in_quotes = false
|
|
74
|
+
quote_char = nil
|
|
75
|
+
in_liquid = 0 # Track nested Liquid expressions
|
|
76
|
+
escaped = false # Track if next character is escaped
|
|
77
|
+
|
|
78
|
+
markup.each_char do |char|
|
|
79
|
+
# Handle escape sequences when in quotes
|
|
80
|
+
if escaped
|
|
81
|
+
current += char # Add the escaped character directly
|
|
82
|
+
escaped = false
|
|
83
|
+
next
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Check for escape character when in quotes
|
|
87
|
+
if char == "\\" && in_quotes
|
|
88
|
+
escaped = true
|
|
89
|
+
next # Don't add backslash to output, it's just the escape marker
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Track Liquid expression boundaries
|
|
93
|
+
if char == "{" && !in_quotes
|
|
94
|
+
in_liquid += 1
|
|
95
|
+
current += char
|
|
96
|
+
elsif char == "}" && !in_quotes && in_liquid > 0
|
|
97
|
+
in_liquid -= 1
|
|
98
|
+
current += char
|
|
99
|
+
# Track quote boundaries
|
|
100
|
+
elsif ['"', "'"].include?(char) && !in_quotes && in_liquid == 0
|
|
101
|
+
in_quotes = true
|
|
102
|
+
quote_char = char
|
|
103
|
+
current += char
|
|
104
|
+
elsif char == quote_char && in_quotes
|
|
105
|
+
in_quotes = false
|
|
106
|
+
current += char
|
|
107
|
+
quote_char = nil
|
|
108
|
+
# Split on whitespace only if not in quotes or Liquid expression
|
|
109
|
+
elsif char.match?(/\s/) && !in_quotes && in_liquid == 0
|
|
110
|
+
tokens << current unless current.empty?
|
|
111
|
+
current = ""
|
|
112
|
+
else
|
|
113
|
+
current += char
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
tokens << current unless current.empty?
|
|
117
|
+
|
|
118
|
+
# First token is URL, remaining tokens may be title or archive parameter
|
|
119
|
+
result = {
|
|
120
|
+
url: tokens.shift,
|
|
121
|
+
title: nil,
|
|
122
|
+
archive: nil
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
# Process remaining tokens
|
|
126
|
+
tokens.each do |token|
|
|
127
|
+
if token.start_with?("archive:")
|
|
128
|
+
result[:archive] = token.sub(/^archive:/, "")
|
|
129
|
+
else
|
|
130
|
+
# Accumulate title tokens
|
|
131
|
+
result[:title] = result[:title].nil? ? token : "#{result[:title]} #{token}"
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
result
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Resolve URL from token (may be Liquid expression or literal)
|
|
139
|
+
#
|
|
140
|
+
# @param token [String] the URL token
|
|
141
|
+
# @param context [Liquid::Context] the Liquid context
|
|
142
|
+
# @return [String] resolved URL
|
|
143
|
+
def resolve_url(token, context)
|
|
144
|
+
return nil if token.nil? || token.empty?
|
|
145
|
+
|
|
146
|
+
evaluate_expression(token, context, allow_nil: false)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Resolve title from source (may be Liquid expression or literal)
|
|
150
|
+
#
|
|
151
|
+
# @param source [String, nil] the title source
|
|
152
|
+
# @param context [Liquid::Context] the Liquid context
|
|
153
|
+
# @return [String, nil] resolved title
|
|
154
|
+
def resolve_title(source, context)
|
|
155
|
+
return nil if source.nil? || source.empty?
|
|
156
|
+
|
|
157
|
+
evaluate_expression(source, context, allow_nil: true)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Resolve archive URL (may be explicit, auto-lookup, or opt-out)
|
|
161
|
+
#
|
|
162
|
+
# @param source [String, nil] the archive source
|
|
163
|
+
# @param context [Liquid::Context] the Liquid context
|
|
164
|
+
# @param url [String] the target URL to archive
|
|
165
|
+
# @return [String, nil] resolved archive URL
|
|
166
|
+
def resolve_archive(source, context, url)
|
|
167
|
+
# Check for explicit opt-out
|
|
168
|
+
return nil if source && source.downcase == "none"
|
|
169
|
+
|
|
170
|
+
# Check for explicit archive URL
|
|
171
|
+
return evaluate_expression(source, context, allow_nil: true) if source && !source.empty?
|
|
172
|
+
|
|
173
|
+
# Auto-lookup if enabled
|
|
174
|
+
return archive_url_for(url) if archive_enabled?
|
|
175
|
+
|
|
176
|
+
nil
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Build template variables hash for rendering
|
|
180
|
+
#
|
|
181
|
+
# @param url [String] the link URL
|
|
182
|
+
# @param title [String, nil] the title text
|
|
183
|
+
# @param archive_url [String, nil] the archive URL
|
|
184
|
+
# @return [Hash] template variables with raw and escaped versions
|
|
185
|
+
def build_template_variables(url, title, archive_url)
|
|
186
|
+
display_url = strip_protocol(url)
|
|
187
|
+
|
|
188
|
+
{
|
|
189
|
+
"url" => url,
|
|
190
|
+
"display_url" => display_url,
|
|
191
|
+
"title" => title,
|
|
192
|
+
"archive_url" => archive_url,
|
|
193
|
+
"escaped_url" => CGI.escapeHTML(url),
|
|
194
|
+
"escaped_display_url" => CGI.escapeHTML(display_url),
|
|
195
|
+
"escaped_title" => title ? CGI.escapeHTML(title) : nil,
|
|
196
|
+
"escaped_archive_url" => archive_url ? CGI.escapeHTML(archive_url) : nil
|
|
197
|
+
}
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Strip protocol from URL for display
|
|
201
|
+
#
|
|
202
|
+
# @param url [String] the URL
|
|
203
|
+
# @return [String] URL without protocol
|
|
204
|
+
def strip_protocol(url)
|
|
205
|
+
url.sub(%r{^https?://}, "")
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Register the tag with Liquid
|
|
211
|
+
Liquid::Template.register_tag("linkcard", JekyllHighlightCards::LinkcardTag)
|