jekyll-highlight-cards 2.0.0 → 2.1.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 +7 -0
- data/jekyll-highlight-cards.gemspec +4 -2
- data/lib/jekyll-highlight-cards/archive_helper.rb +23 -52
- 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/image_sizing_hooks.rb +53 -62
- data/lib/jekyll-highlight-cards/linkcard_tag.rb +28 -52
- data/lib/jekyll-highlight-cards/polaroid_tag.rb +41 -80
- data/lib/jekyll-highlight-cards/template_renderer.rb +22 -20
- data/lib/jekyll-highlight-cards/version.rb +1 -1
- metadata +33 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2e7d4bb8027142ccb1dbf7e0327098460eb49fda5f10fa2619efeb9053fa1359
|
|
4
|
+
data.tar.gz: 9971cb5b3a34a9bfbfaf06539e973df20c5951fb5bac39649378c4909a03a759
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d1183485eba6958f3d328b693207cfbccfd0dcef3da163511332c0ad16b39c30416b8c1bb41e767b9dc8ddee5d761f92ae26bb7b56f18ada1b5d1ef8a5bbfa21
|
|
7
|
+
data.tar.gz: 9d62f2b98e4923cdd73a75719718d92dd40b235dd0c61982e4222bc4a1f525b9593ddef3b10f3485ea56f30ad300e0be6310f8a6ccbfcc6ac50c1448092cd8d4
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [2.1.0](https://github.com/Texarkanine/jekyll-highlight-cards/compare/v2.0.0...v2.1.0) (2026-07-19)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **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))
|
|
9
|
+
|
|
3
10
|
## [2.0.0](https://github.com/Texarkanine/jekyll-highlight-cards/compare/v1.1.2...v2.0.0) (2026-01-18)
|
|
4
11
|
|
|
5
12
|
|
|
@@ -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
|
|
@@ -26,21 +26,12 @@ module JekyllHighlightCards
|
|
|
26
26
|
# @return [String, nil] the archive URL, or nil if not found
|
|
27
27
|
def archive_url_for(url)
|
|
28
28
|
ArchiveHelper.archive_cache[url] ||= begin
|
|
29
|
-
log_info("Looking up archive for #{url}")
|
|
30
29
|
archive_url = lookup_archive(url)
|
|
31
|
-
log_info("Archive URL: #{archive_url || ""}")
|
|
32
30
|
|
|
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
|
|
31
|
+
archive_url = submit_archive(url) || archive_url if archive_save_enabled?
|
|
38
32
|
|
|
39
|
-
archive_url
|
|
33
|
+
archive_url
|
|
40
34
|
end
|
|
41
|
-
rescue StandardError => e
|
|
42
|
-
log_debug("Archive lookup failed for #{url}: #{e.message}")
|
|
43
|
-
nil
|
|
44
35
|
end
|
|
45
36
|
|
|
46
37
|
# Check if archiving is enabled via environment variables
|
|
@@ -72,14 +63,10 @@ module JekyllHighlightCards
|
|
|
72
63
|
# @param url [String] the URL to look up
|
|
73
64
|
# @return [String, nil] the archive URL if found, nil otherwise
|
|
74
65
|
def lookup_archive(url)
|
|
75
|
-
log_debug("lookup_archive(#{url})")
|
|
76
|
-
|
|
77
66
|
encoded_url = URI.encode_www_form_component(url)
|
|
78
67
|
cdx_url_str = "https://web.archive.org/cdx/search/cdx?url=#{encoded_url}&output=json&filter=statuscode:200&limit=-1&fl=timestamp,original"
|
|
79
68
|
cdx_url = URI.parse(cdx_url_str)
|
|
80
69
|
|
|
81
|
-
log_debug("CDX lookup URL: #{cdx_url_str}")
|
|
82
|
-
|
|
83
70
|
response = Net::HTTP.start(
|
|
84
71
|
cdx_url.host,
|
|
85
72
|
cdx_url.port,
|
|
@@ -90,25 +77,15 @@ module JekyllHighlightCards
|
|
|
90
77
|
http.request(Net::HTTP::Get.new(cdx_url.request_uri))
|
|
91
78
|
end
|
|
92
79
|
|
|
93
|
-
unless response.is_a?(Net::HTTPSuccess)
|
|
94
|
-
log_debug("CDX lookup failed: #{response.code} #{response.message}")
|
|
95
|
-
return nil
|
|
96
|
-
end
|
|
80
|
+
return nil unless response.is_a?(Net::HTTPSuccess)
|
|
97
81
|
|
|
98
|
-
log_debug("CDX lookup found archived page...")
|
|
99
82
|
rows = JSON.parse(response.body)
|
|
100
83
|
|
|
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}"
|
|
84
|
+
return nil if rows.size < 2
|
|
107
85
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
rescue StandardError
|
|
111
|
-
log_debug("CDX lookup error for #{url}: #{e.message}")
|
|
86
|
+
timestamp, = rows.last
|
|
87
|
+
"https://web.archive.org/web/#{timestamp}/#{url}"
|
|
88
|
+
rescue StandardError
|
|
112
89
|
nil
|
|
113
90
|
end
|
|
114
91
|
|
|
@@ -117,35 +94,29 @@ module JekyllHighlightCards
|
|
|
117
94
|
# @param url [String] the URL to submit for archiving
|
|
118
95
|
# @return [String, nil] the archive URL if successful, nil otherwise
|
|
119
96
|
def submit_archive(url)
|
|
120
|
-
log_debug("submit_archive(#{url})")
|
|
121
|
-
|
|
122
97
|
encoded_url = URI.encode_www_form_component(url)
|
|
123
98
|
save_url = URI.parse("https://web.archive.org/save/#{encoded_url}")
|
|
124
99
|
|
|
125
|
-
response =
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
100
|
+
response = begin
|
|
101
|
+
Net::HTTP.start(
|
|
102
|
+
save_url.host,
|
|
103
|
+
save_url.port,
|
|
104
|
+
use_ssl: save_url.scheme == "https",
|
|
105
|
+
open_timeout: 10,
|
|
106
|
+
read_timeout: 30
|
|
107
|
+
) do |http|
|
|
108
|
+
req = Net::HTTP::Get.new(save_url.request_uri, { "User-Agent" => archive_user_agent })
|
|
109
|
+
http.request(req)
|
|
110
|
+
end
|
|
111
|
+
rescue StandardError
|
|
112
|
+
nil
|
|
134
113
|
end
|
|
114
|
+
return nil if response.nil?
|
|
135
115
|
|
|
136
116
|
location = response["content-location"]
|
|
117
|
+
return nil if location.to_s.empty?
|
|
137
118
|
|
|
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
|
|
119
|
+
"https://web.archive.org#{location}"
|
|
149
120
|
end
|
|
150
121
|
end
|
|
151
122
|
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
|
|
@@ -21,12 +21,27 @@ module JekyllHighlightCards
|
|
|
21
21
|
# @note Images inside code fences and inline code are not processed
|
|
22
22
|
# @note Sized images are automatically wrapped in <a> tags (if not already)
|
|
23
23
|
module ImageSizingHooks
|
|
24
|
-
extend DimensionParser
|
|
25
|
-
|
|
26
24
|
# Process document content before rendering
|
|
27
25
|
# Converts  to <!-- IMG_SIZE:W:H -->
|
|
28
26
|
#
|
|
29
27
|
# @param document [Jekyll::Document] the document being processed
|
|
28
|
+
SIZED_MARKDOWN_PATTERN = /!\[([^\]]*)\]\(([^)]+)[ \t]+=([^)]+)\)/
|
|
29
|
+
SIZED_IMG_HTML_PATTERN = /(\s*)(<img[ \t]+[^>]*>)\s*<!--\s*IMG_SIZE:([^:]*):([^:]*)\s*-->/
|
|
30
|
+
|
|
31
|
+
# @param prefix [String] HTML preceding an image tag
|
|
32
|
+
# @return [Integer] unclosed anchor tags in +prefix+
|
|
33
|
+
def self.unclosed_anchor_count(prefix)
|
|
34
|
+
prefix.scan("<a ").length + prefix.scan("<a\t").length - prefix.scan("</a>").length
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def self.markdown_inline_check_position(full_match)
|
|
38
|
+
full_match.begin(0)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.img_link_prefix(output, match)
|
|
42
|
+
output[0, match.begin(2)]
|
|
43
|
+
end
|
|
44
|
+
|
|
30
45
|
def self.process_pre_render(document)
|
|
31
46
|
content = document.content
|
|
32
47
|
return unless content
|
|
@@ -42,23 +57,24 @@ module JekyllHighlightCards
|
|
|
42
57
|
end
|
|
43
58
|
|
|
44
59
|
# Process the line to convert sized images
|
|
45
|
-
processed_line = line
|
|
60
|
+
processed_line = line
|
|
46
61
|
|
|
47
62
|
# Match  pattern
|
|
48
63
|
# Use gsub with block to check each match
|
|
49
|
-
processed_line.gsub!(
|
|
50
|
-
|
|
64
|
+
processed_line.gsub!(SIZED_MARKDOWN_PATTERN) do |match|
|
|
65
|
+
full_match = Regexp.last_match
|
|
66
|
+
match_start = markdown_inline_check_position(full_match)
|
|
51
67
|
|
|
52
68
|
# Skip if inside inline code
|
|
53
69
|
if in_inline_code?(line, match_start)
|
|
54
70
|
match
|
|
55
71
|
else
|
|
56
|
-
alt =
|
|
57
|
-
src =
|
|
58
|
-
size =
|
|
72
|
+
alt = full_match[1]
|
|
73
|
+
src = full_match[2].strip
|
|
74
|
+
size = full_match[3].strip
|
|
59
75
|
|
|
60
76
|
# Parse dimensions using DimensionParser
|
|
61
|
-
width, height = parse_dimensions(size)
|
|
77
|
+
width, height = DimensionParser.parse_dimensions(size)
|
|
62
78
|
|
|
63
79
|
# Build marker comment
|
|
64
80
|
"<!-- IMG_SIZE:#{width}:#{height} -->"
|
|
@@ -83,82 +99,63 @@ module JekyllHighlightCards
|
|
|
83
99
|
output = output.dup
|
|
84
100
|
|
|
85
101
|
# Match <img><!-- IMG_SIZE:W:H --> patterns
|
|
86
|
-
output.gsub!(
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
102
|
+
output.gsub!(SIZED_IMG_HTML_PATTERN) do
|
|
103
|
+
match = Regexp.last_match
|
|
104
|
+
img_tag = match[2]
|
|
105
|
+
width = match[3].strip
|
|
106
|
+
height = match[4].strip
|
|
90
107
|
|
|
91
|
-
# Build attributes to add
|
|
92
108
|
attrs = []
|
|
93
|
-
attrs << %(width="#{width}") unless width.
|
|
94
|
-
attrs << %(height="#{height}") unless height.
|
|
109
|
+
attrs << %(width="#{width}") unless width.empty?
|
|
110
|
+
attrs << %(height="#{height}") unless height.empty?
|
|
95
111
|
|
|
96
|
-
# Add attributes to img tag
|
|
97
112
|
modified_img = if attrs.any?
|
|
98
113
|
img_tag.sub("<img", "<img #{attrs.join(" ")}")
|
|
99
114
|
else
|
|
100
115
|
img_tag
|
|
101
116
|
end
|
|
102
117
|
|
|
103
|
-
# Extract src for auto-linking
|
|
104
118
|
src = img_tag[/src=["']([^"']+)["']/, 1]
|
|
119
|
+
next modified_img if src.nil?
|
|
105
120
|
|
|
106
|
-
|
|
107
|
-
next modified_img if src.nil? || src.empty?
|
|
121
|
+
prefix = img_link_prefix(output, match)
|
|
108
122
|
|
|
109
|
-
|
|
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)
|
|
123
|
+
already_linked = unclosed_anchor_count(prefix).positive?
|
|
118
124
|
|
|
119
125
|
# Auto-link if not already linked
|
|
120
126
|
if already_linked
|
|
121
127
|
modified_img
|
|
122
128
|
else
|
|
123
|
-
%(<a href="#{CGI.escapeHTML(src)}">#{modified_img}</a>)
|
|
129
|
+
%(<a href="#{CGI.escapeHTML(CGI.unescapeHTML(src))}">#{modified_img}</a>)
|
|
124
130
|
end
|
|
125
131
|
end
|
|
126
132
|
|
|
127
133
|
document.output = output
|
|
128
134
|
end
|
|
129
135
|
|
|
136
|
+
def self.fence_count_before(lines, line_idx)
|
|
137
|
+
lines.first(line_idx).count { |line| line.start_with?("```", "~~~") }
|
|
138
|
+
end
|
|
139
|
+
|
|
130
140
|
# Check if a line is inside a code fence
|
|
131
141
|
#
|
|
132
142
|
# @param lines [Array<String>] all lines in the document
|
|
133
143
|
# @param line_idx [Integer] the current line index
|
|
134
144
|
# @return [Boolean] true if inside code fence
|
|
135
145
|
def self.in_code_fence?(lines, line_idx)
|
|
136
|
-
|
|
137
|
-
|
|
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.positive?
|
|
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
|
|
146
|
+
return true if fence_count_before(lines, line_idx).odd?
|
|
147
|
+
return false unless line_idx.positive?
|
|
159
148
|
|
|
160
|
-
|
|
161
|
-
|
|
149
|
+
lines.slice(line_idx).start_with?(" ", "\t")
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Count backtick characters before a position in a line
|
|
153
|
+
#
|
|
154
|
+
# @param line [String] the line of text
|
|
155
|
+
# @param position [Integer] the position in the line
|
|
156
|
+
# @return [Integer] number of backticks before +position+
|
|
157
|
+
def self.backtick_count_before(line, position)
|
|
158
|
+
line[0, position].count("`")
|
|
162
159
|
end
|
|
163
160
|
|
|
164
161
|
# Check if text position is inside inline code
|
|
@@ -167,13 +164,7 @@ module JekyllHighlightCards
|
|
|
167
164
|
# @param position [Integer] the position in the line
|
|
168
165
|
# @return [Boolean] true if inside inline code
|
|
169
166
|
def self.in_inline_code?(line, position)
|
|
170
|
-
|
|
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?
|
|
167
|
+
backtick_count_before(line, position).odd?
|
|
177
168
|
end
|
|
178
169
|
end
|
|
179
170
|
end
|
|
@@ -26,10 +26,6 @@ module JekyllHighlightCards
|
|
|
26
26
|
# @param tag_name [String] the name of the tag
|
|
27
27
|
# @param markup [String] the tag markup containing parameters
|
|
28
28
|
# @param tokens [Array] parse tokens (unused)
|
|
29
|
-
def initialize(tag_name, markup, tokens)
|
|
30
|
-
super
|
|
31
|
-
@markup = markup.strip
|
|
32
|
-
end
|
|
33
29
|
|
|
34
30
|
# Render the linkcard tag
|
|
35
31
|
#
|
|
@@ -40,7 +36,7 @@ module JekyllHighlightCards
|
|
|
40
36
|
parsed = split_markup(@markup)
|
|
41
37
|
|
|
42
38
|
# Resolve URL (required)
|
|
43
|
-
url = resolve_url(parsed
|
|
39
|
+
url = resolve_url(parsed.fetch(:url), context)
|
|
44
40
|
raise ArgumentError, "linkcard tag requires a URL" if url.nil? || url.empty?
|
|
45
41
|
|
|
46
42
|
# Resolve title (optional)
|
|
@@ -66,71 +62,60 @@ module JekyllHighlightCards
|
|
|
66
62
|
# @param markup [String] the tag markup
|
|
67
63
|
# @return [Hash] parsed components
|
|
68
64
|
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
65
|
tokens = []
|
|
72
66
|
current = ""
|
|
73
67
|
in_quotes = false
|
|
74
68
|
quote_char = nil
|
|
75
|
-
in_liquid = 0
|
|
76
|
-
escaped = false
|
|
69
|
+
in_liquid = 0
|
|
70
|
+
escaped = false
|
|
77
71
|
|
|
78
|
-
markup.each_char do |char|
|
|
79
|
-
# Handle escape sequences when in quotes
|
|
72
|
+
"#{markup} ".each_char do |char|
|
|
80
73
|
if escaped
|
|
81
|
-
current += char
|
|
74
|
+
current += char
|
|
82
75
|
escaped = false
|
|
83
76
|
next
|
|
84
77
|
end
|
|
85
78
|
|
|
86
|
-
# Check for escape character when in quotes
|
|
87
79
|
if char == "\\" && in_quotes
|
|
88
80
|
escaped = true
|
|
89
|
-
next
|
|
81
|
+
next
|
|
90
82
|
end
|
|
91
83
|
|
|
92
|
-
# Track Liquid expression boundaries
|
|
93
84
|
if char == "{" && !in_quotes
|
|
94
85
|
in_liquid += 1
|
|
95
86
|
current += char
|
|
96
|
-
elsif char == "}" &&
|
|
87
|
+
elsif char == "}" && in_liquid.positive?
|
|
97
88
|
in_liquid -= 1
|
|
98
89
|
current += char
|
|
99
|
-
|
|
100
|
-
elsif ['"', "'"].include?(char) && !in_quotes && in_liquid.zero?
|
|
90
|
+
elsif char == '"' && !in_quotes
|
|
101
91
|
in_quotes = true
|
|
102
92
|
quote_char = char
|
|
103
93
|
current += char
|
|
104
|
-
elsif char == quote_char
|
|
94
|
+
elsif char == quote_char
|
|
105
95
|
in_quotes = false
|
|
106
96
|
current += char
|
|
107
97
|
quote_char = nil
|
|
108
|
-
# Split on whitespace only if not in quotes or Liquid expression
|
|
109
98
|
elsif char.match?(/\s/) && !in_quotes && in_liquid.zero?
|
|
110
|
-
tokens << current
|
|
99
|
+
tokens << current
|
|
111
100
|
current = ""
|
|
112
101
|
else
|
|
113
102
|
current += char
|
|
114
103
|
end
|
|
115
104
|
end
|
|
116
|
-
tokens
|
|
105
|
+
tokens.reject!(&:empty?)
|
|
117
106
|
|
|
118
|
-
|
|
119
|
-
result =
|
|
120
|
-
url: tokens.shift,
|
|
121
|
-
title: nil,
|
|
122
|
-
archive: nil
|
|
123
|
-
}
|
|
107
|
+
result = {}
|
|
108
|
+
result[:url] = tokens.shift
|
|
124
109
|
|
|
125
|
-
|
|
110
|
+
title_tokens = []
|
|
126
111
|
tokens.each do |token|
|
|
127
112
|
if token.start_with?("archive:")
|
|
128
|
-
result[:archive] = token.
|
|
113
|
+
result[:archive] = token.delete_prefix("archive:")
|
|
129
114
|
else
|
|
130
|
-
|
|
131
|
-
result[:title] = result[:title].nil? ? token : "#{result[:title]} #{token}"
|
|
115
|
+
title_tokens << token
|
|
132
116
|
end
|
|
133
117
|
end
|
|
118
|
+
result[:title] = title_tokens.join(" ") unless title_tokens.empty?
|
|
134
119
|
|
|
135
120
|
result
|
|
136
121
|
end
|
|
@@ -141,9 +126,7 @@ module JekyllHighlightCards
|
|
|
141
126
|
# @param context [Liquid::Context] the Liquid context
|
|
142
127
|
# @return [String] resolved URL
|
|
143
128
|
def resolve_url(token, context)
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
evaluate_expression(token, context, allow_nil: false)
|
|
129
|
+
evaluate_expression(token, context)
|
|
147
130
|
end
|
|
148
131
|
|
|
149
132
|
# Resolve title from source (may be Liquid expression or literal)
|
|
@@ -152,9 +135,7 @@ module JekyllHighlightCards
|
|
|
152
135
|
# @param context [Liquid::Context] the Liquid context
|
|
153
136
|
# @return [String, nil] resolved title
|
|
154
137
|
def resolve_title(source, context)
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
evaluate_expression(source, context, allow_nil: true)
|
|
138
|
+
evaluate_expression(source, context)
|
|
158
139
|
end
|
|
159
140
|
|
|
160
141
|
# Resolve archive URL (may be explicit, auto-lookup, or opt-out)
|
|
@@ -168,12 +149,9 @@ module JekyllHighlightCards
|
|
|
168
149
|
return nil if source && source.downcase == "none"
|
|
169
150
|
|
|
170
151
|
# Check for explicit archive URL
|
|
171
|
-
return evaluate_expression(source, context
|
|
172
|
-
|
|
173
|
-
# Auto-lookup if enabled
|
|
174
|
-
return archive_url_for(url) if archive_enabled?
|
|
152
|
+
return evaluate_expression(source, context) if source && !source.empty?
|
|
175
153
|
|
|
176
|
-
|
|
154
|
+
archive_enabled? && archive_url_for(url)
|
|
177
155
|
end
|
|
178
156
|
|
|
179
157
|
# Build template variables hash for rendering
|
|
@@ -183,19 +161,17 @@ module JekyllHighlightCards
|
|
|
183
161
|
# @param archive_url [String, nil] the archive URL
|
|
184
162
|
# @return [Hash] template variables with raw and escaped versions
|
|
185
163
|
def build_template_variables(url, title, archive_url)
|
|
186
|
-
display_url = strip_protocol(url)
|
|
187
|
-
display_url = display_url.sub(%r{/$}, "")
|
|
164
|
+
display_url = strip_protocol(url).delete_suffix("/")
|
|
188
165
|
|
|
189
|
-
{
|
|
190
|
-
"url" => url,
|
|
191
|
-
"display_url" => display_url,
|
|
166
|
+
variables = {
|
|
192
167
|
"title" => title,
|
|
193
168
|
"archive_url" => archive_url,
|
|
194
169
|
"escaped_url" => CGI.escapeHTML(url),
|
|
195
|
-
"escaped_display_url" => CGI.escapeHTML(display_url)
|
|
196
|
-
"escaped_title" => title ? CGI.escapeHTML(title) : nil,
|
|
197
|
-
"escaped_archive_url" => archive_url ? CGI.escapeHTML(archive_url) : nil
|
|
170
|
+
"escaped_display_url" => CGI.escapeHTML(display_url)
|
|
198
171
|
}
|
|
172
|
+
variables["escaped_title"] = CGI.escapeHTML(title) if title
|
|
173
|
+
variables["escaped_archive_url"] = CGI.escapeHTML(archive_url) if archive_url
|
|
174
|
+
variables
|
|
199
175
|
end
|
|
200
176
|
|
|
201
177
|
# Strip protocol from URL for display
|
|
@@ -203,7 +179,7 @@ module JekyllHighlightCards
|
|
|
203
179
|
# @param url [String] the URL
|
|
204
180
|
# @return [String] URL without protocol
|
|
205
181
|
def strip_protocol(url)
|
|
206
|
-
url.
|
|
182
|
+
url.delete_prefix("https://").delete_prefix("http://")
|
|
207
183
|
end
|
|
208
184
|
end
|
|
209
185
|
end
|
|
@@ -33,53 +33,35 @@ module JekyllHighlightCards
|
|
|
33
33
|
# {% polaroid /img.jpg link="https://example.com" image_link="https://other.com" %}
|
|
34
34
|
class PolaroidTag < Liquid::Tag
|
|
35
35
|
include ArchiveHelper
|
|
36
|
-
include DimensionParser
|
|
37
36
|
include ExpressionEvaluator
|
|
38
37
|
include TemplateRenderer
|
|
39
38
|
|
|
40
|
-
# Initialize the tag
|
|
41
|
-
#
|
|
42
|
-
# @param tag_name [String] the name of the tag
|
|
43
|
-
# @param markup [String] the tag markup containing parameters
|
|
44
|
-
# @param tokens [Array] parse tokens (unused)
|
|
45
|
-
def initialize(tag_name, markup, tokens)
|
|
46
|
-
super
|
|
47
|
-
@markup = markup.strip
|
|
48
|
-
end
|
|
49
|
-
|
|
50
39
|
# Render the polaroid tag
|
|
51
40
|
#
|
|
52
41
|
# @param context [Liquid::Context] the Liquid rendering context
|
|
53
42
|
# @return [String] rendered HTML
|
|
54
43
|
def render(context)
|
|
55
|
-
# Parse markup
|
|
56
44
|
params = parse_markup(@markup, context)
|
|
57
45
|
|
|
58
|
-
|
|
59
|
-
raise ArgumentError, "polaroid tag requires an image URL" if params[:image_url].nil? || params[:image_url].empty?
|
|
60
|
-
|
|
61
|
-
# Parse size parameter if present
|
|
62
|
-
width, height = parse_dimensions(params[:size]) if params[:size]
|
|
46
|
+
raise ArgumentError, "polaroid tag requires an image URL" if params.fetch(:image_url).empty?
|
|
63
47
|
|
|
64
|
-
|
|
65
|
-
# Track if link was explicitly provided
|
|
66
|
-
explicit_link = !params[:link].nil? && !params[:link].empty?
|
|
67
|
-
link_url = params[:link] || params[:image_url]
|
|
48
|
+
width, height = DimensionParser.parse_dimensions(params[:size])
|
|
68
49
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
50
|
+
explicit_link = !params[:link].to_s.empty?
|
|
51
|
+
link_url = if params[:link].to_s.empty?
|
|
52
|
+
params.fetch(:image_url)
|
|
53
|
+
else
|
|
54
|
+
params.fetch(:link)
|
|
55
|
+
end
|
|
56
|
+
image_link_url = if params[:image_link].to_s.empty?
|
|
74
57
|
link_url
|
|
58
|
+
else
|
|
59
|
+
params.fetch(:image_link)
|
|
75
60
|
end
|
|
61
|
+
archive_url = resolve_archive(params[:archive], link_url)
|
|
76
62
|
|
|
77
|
-
# Resolve archive URL (archives the link URL, not the image)
|
|
78
|
-
archive_url = resolve_archive(params[:archive], context, link_url)
|
|
79
|
-
|
|
80
|
-
# Build template variables
|
|
81
63
|
variables = build_template_variables(
|
|
82
|
-
params
|
|
64
|
+
params.fetch(:image_url),
|
|
83
65
|
width,
|
|
84
66
|
height,
|
|
85
67
|
params[:title],
|
|
@@ -90,11 +72,7 @@ module JekyllHighlightCards
|
|
|
90
72
|
archive_url
|
|
91
73
|
)
|
|
92
74
|
|
|
93
|
-
|
|
94
|
-
site = context.registers[:site]
|
|
95
|
-
|
|
96
|
-
# Render template
|
|
97
|
-
render_template(site, "polaroid", variables)
|
|
75
|
+
render_template(context.registers[:site], "polaroid", variables)
|
|
98
76
|
end
|
|
99
77
|
|
|
100
78
|
private
|
|
@@ -105,8 +83,6 @@ module JekyllHighlightCards
|
|
|
105
83
|
# @param context [Liquid::Context] the Liquid context
|
|
106
84
|
# @return [Hash] parsed parameters
|
|
107
85
|
def parse_markup(markup, context)
|
|
108
|
-
# Split by whitespace, keeping quoted strings and Liquid expressions together
|
|
109
|
-
# Handles escaped quotes (\") and backslashes (\\) within quoted strings
|
|
110
86
|
tokens = []
|
|
111
87
|
current = ""
|
|
112
88
|
in_quotes = false
|
|
@@ -114,82 +90,67 @@ module JekyllHighlightCards
|
|
|
114
90
|
in_liquid = 0
|
|
115
91
|
escaped = false
|
|
116
92
|
|
|
117
|
-
|
|
118
|
-
|
|
93
|
+
# Trailing space flushes the final token through the whitespace branch
|
|
94
|
+
"#{markup} ".each_char do |char|
|
|
119
95
|
if escaped
|
|
120
|
-
current += char
|
|
96
|
+
current += char
|
|
121
97
|
escaped = false
|
|
122
98
|
next
|
|
123
99
|
end
|
|
124
100
|
|
|
125
|
-
# Check for escape character when in quotes
|
|
126
101
|
if char == "\\" && in_quotes
|
|
127
102
|
escaped = true
|
|
128
|
-
next
|
|
103
|
+
next
|
|
129
104
|
end
|
|
130
105
|
|
|
131
|
-
# Track Liquid expression boundaries
|
|
132
106
|
if char == "{" && !in_quotes
|
|
133
107
|
in_liquid += 1
|
|
134
108
|
current += char
|
|
135
|
-
elsif char == "}" &&
|
|
109
|
+
elsif char == "}" && in_liquid.positive?
|
|
136
110
|
in_liquid -= 1
|
|
137
111
|
current += char
|
|
138
|
-
|
|
139
|
-
elsif ['"', "'"].include?(char) && !in_quotes && in_liquid.zero?
|
|
112
|
+
elsif ['"', "'"].include?(char) && !in_quotes
|
|
140
113
|
in_quotes = true
|
|
141
114
|
quote_char = char
|
|
142
115
|
current += char
|
|
143
|
-
elsif char == quote_char
|
|
116
|
+
elsif char == quote_char
|
|
144
117
|
in_quotes = false
|
|
145
118
|
current += char
|
|
146
119
|
quote_char = nil
|
|
147
|
-
# Split on whitespace only if not in quotes or Liquid expression
|
|
148
120
|
elsif char.match?(/\s/) && !in_quotes && in_liquid.zero?
|
|
149
|
-
tokens << current
|
|
121
|
+
tokens << current
|
|
150
122
|
current = ""
|
|
151
123
|
else
|
|
152
124
|
current += char
|
|
153
125
|
end
|
|
154
126
|
end
|
|
155
|
-
tokens << current unless current.empty?
|
|
156
127
|
|
|
157
|
-
# First token is image URL (required)
|
|
158
128
|
image_url_token = tokens.shift
|
|
159
|
-
image_url = evaluate_expression(image_url_token, context
|
|
129
|
+
image_url = evaluate_expression(image_url_token, context)
|
|
160
130
|
|
|
161
|
-
# Parse remaining tokens as key=value pairs
|
|
162
131
|
result = { image_url: image_url }
|
|
163
132
|
tokens.each do |token|
|
|
164
|
-
next unless token =~
|
|
133
|
+
next unless token =~ /\A(\w+)=(.+)\z/
|
|
165
134
|
|
|
166
135
|
key = Regexp.last_match(1).to_sym
|
|
167
136
|
value_token = Regexp.last_match(2)
|
|
168
|
-
|
|
169
|
-
# Evaluate value as Liquid expression
|
|
170
|
-
result[key] = evaluate_expression(value_token, context, allow_nil: true)
|
|
137
|
+
result[key] = evaluate_expression(value_token, context)
|
|
171
138
|
end
|
|
172
139
|
|
|
173
140
|
result
|
|
174
141
|
end
|
|
175
142
|
|
|
176
|
-
# Resolve archive URL (may be explicit, auto-lookup, or opt-out)
|
|
143
|
+
# Resolve archive URL (may be explicit, auto-lookup, or opt-out).
|
|
144
|
+
# Named archive params are already Liquid-evaluated in parse_markup.
|
|
177
145
|
#
|
|
178
146
|
# @param source [String, nil] the archive source
|
|
179
|
-
# @param context [Liquid::Context] the Liquid context (unused but kept for consistency)
|
|
180
147
|
# @param url [String] the target URL to archive
|
|
181
148
|
# @return [String, nil] resolved archive URL
|
|
182
|
-
def resolve_archive(source,
|
|
183
|
-
|
|
184
|
-
return
|
|
185
|
-
|
|
186
|
-
# Check for explicit archive URL
|
|
187
|
-
return evaluate_expression(source, context, allow_nil: true) if source && !source.empty?
|
|
188
|
-
|
|
189
|
-
# Auto-lookup if enabled (archives the link URL, not the image)
|
|
190
|
-
return archive_url_for(url) if archive_enabled?
|
|
149
|
+
def resolve_archive(source, url)
|
|
150
|
+
return nil if source && source.downcase == "none"
|
|
151
|
+
return source unless source.to_s.empty?
|
|
191
152
|
|
|
192
|
-
|
|
153
|
+
archive_enabled? && archive_url_for(url)
|
|
193
154
|
end
|
|
194
155
|
|
|
195
156
|
# Build template variables hash for rendering
|
|
@@ -215,12 +176,12 @@ module JekyllHighlightCards
|
|
|
215
176
|
explicit_link,
|
|
216
177
|
archive_url
|
|
217
178
|
)
|
|
218
|
-
|
|
219
|
-
|
|
179
|
+
link_display = strip_protocol(link_url) if explicit_link
|
|
180
|
+
escaped_title = CGI.escapeHTML(title.to_s)
|
|
181
|
+
escaped_title = " " if escaped_title.empty?
|
|
220
182
|
|
|
221
183
|
{
|
|
222
184
|
"image_url" => image_url,
|
|
223
|
-
"link_url" => link_url,
|
|
224
185
|
"image_link_url" => image_link_url,
|
|
225
186
|
"title" => title,
|
|
226
187
|
"alt" => alt,
|
|
@@ -229,12 +190,12 @@ module JekyllHighlightCards
|
|
|
229
190
|
"width" => width,
|
|
230
191
|
"height" => height,
|
|
231
192
|
"escaped_image_url" => CGI.escapeHTML(image_url),
|
|
232
|
-
"escaped_link_url" =>
|
|
233
|
-
"escaped_image_link_url" =>
|
|
234
|
-
"escaped_title" =>
|
|
235
|
-
"escaped_alt" =>
|
|
236
|
-
"escaped_link_display" => link_display &&
|
|
237
|
-
"escaped_archive_url" => archive_url
|
|
193
|
+
"escaped_link_url" => CGI.escapeHTML(link_url),
|
|
194
|
+
"escaped_image_link_url" => CGI.escapeHTML(image_link_url),
|
|
195
|
+
"escaped_title" => escaped_title,
|
|
196
|
+
"escaped_alt" => CGI.escapeHTML(alt.to_s),
|
|
197
|
+
"escaped_link_display" => link_display && CGI.escapeHTML(link_display),
|
|
198
|
+
"escaped_archive_url" => archive_url && CGI.escapeHTML(archive_url)
|
|
238
199
|
}
|
|
239
200
|
end
|
|
240
201
|
|
|
@@ -243,7 +204,7 @@ module JekyllHighlightCards
|
|
|
243
204
|
# @param url [String] the URL
|
|
244
205
|
# @return [String] URL without protocol
|
|
245
206
|
def strip_protocol(url)
|
|
246
|
-
url.sub(%r{
|
|
207
|
+
url.sub(%r{\Ahttps?://}, "")
|
|
247
208
|
end
|
|
248
209
|
end
|
|
249
210
|
end
|
|
@@ -27,13 +27,14 @@ module JekyllHighlightCards
|
|
|
27
27
|
template = Liquid::Template.parse(template_content)
|
|
28
28
|
template.render(variables)
|
|
29
29
|
rescue Errno::ENOENT, Errno::EACCES => e
|
|
30
|
-
raise TemplateRenderError, "Failed to read template '#{template_name}' at #{template_path}: #{e.
|
|
30
|
+
raise TemplateRenderError, "Failed to read template '#{template_name}' at #{template_path}: #{e.class}: #{e}"
|
|
31
31
|
rescue Encoding::InvalidByteSequenceError => e
|
|
32
|
-
raise TemplateRenderError,
|
|
32
|
+
raise TemplateRenderError,
|
|
33
|
+
"Invalid encoding in template '#{template_name}' at #{template_path}: #{e.class}: #{e}"
|
|
33
34
|
rescue Liquid::SyntaxError, Liquid::Error => e
|
|
34
|
-
raise TemplateRenderError, "Liquid error in template '#{template_name}': #{e.
|
|
35
|
+
raise TemplateRenderError, "Liquid error in template '#{template_name}': #{e.class}: #{e}"
|
|
35
36
|
rescue StandardError => e
|
|
36
|
-
raise TemplateRenderError, "Unexpected error rendering template '#{template_name}': #{e.
|
|
37
|
+
raise TemplateRenderError, "Unexpected error rendering template '#{template_name}': #{e.class}: #{e}"
|
|
37
38
|
end
|
|
38
39
|
end
|
|
39
40
|
|
|
@@ -57,6 +58,19 @@ module JekyllHighlightCards
|
|
|
57
58
|
find_gem_template(relative_path)
|
|
58
59
|
end
|
|
59
60
|
|
|
61
|
+
# Verify template path is safe (within allowed directory)
|
|
62
|
+
#
|
|
63
|
+
# @param allowed_dir [String] the allowed base directory
|
|
64
|
+
# @param template_path [String] the template path to check
|
|
65
|
+
# @return [String, nil] template path if safe, nil otherwise
|
|
66
|
+
def safe_template_path(allowed_dir, template_path)
|
|
67
|
+
return nil unless File.exist?(template_path)
|
|
68
|
+
|
|
69
|
+
expanded_dir = File.expand_path(allowed_dir)
|
|
70
|
+
expanded_path = File.expand_path(template_path)
|
|
71
|
+
template_path if expanded_path.start_with?(expanded_dir)
|
|
72
|
+
end
|
|
73
|
+
|
|
60
74
|
private
|
|
61
75
|
|
|
62
76
|
# Validate template name to prevent path traversal attacks
|
|
@@ -76,9 +90,11 @@ module JekyllHighlightCards
|
|
|
76
90
|
# @param relative_path [String] relative path to template
|
|
77
91
|
# @return [String, nil] path to template or nil
|
|
78
92
|
def find_user_template(site, relative_path)
|
|
79
|
-
|
|
93
|
+
source = site&.source
|
|
94
|
+
# Empty source would File.join to "/_includes/..." — skip it explicitly.
|
|
95
|
+
return nil unless source.is_a?(String) && !source.empty?
|
|
80
96
|
|
|
81
|
-
includes_dir = File.join(
|
|
97
|
+
includes_dir = File.join(source, "_includes")
|
|
82
98
|
template_path = File.join(includes_dir, relative_path)
|
|
83
99
|
|
|
84
100
|
safe_template_path(includes_dir, template_path)
|
|
@@ -95,19 +111,5 @@ module JekyllHighlightCards
|
|
|
95
111
|
|
|
96
112
|
safe_template_path(includes_dir, template_path)
|
|
97
113
|
end
|
|
98
|
-
|
|
99
|
-
# Verify template path is safe (within allowed directory)
|
|
100
|
-
#
|
|
101
|
-
# @param allowed_dir [String] the allowed base directory
|
|
102
|
-
# @param template_path [String] the template path to check
|
|
103
|
-
# @return [String, nil] template path if safe, nil otherwise
|
|
104
|
-
def safe_template_path(allowed_dir, template_path)
|
|
105
|
-
return nil unless File.exist?(template_path)
|
|
106
|
-
|
|
107
|
-
expanded_dir = File.expand_path(allowed_dir)
|
|
108
|
-
expanded_path = File.expand_path(template_path)
|
|
109
|
-
|
|
110
|
-
expanded_path.start_with?(expanded_dir) ? template_path : nil
|
|
111
|
-
end
|
|
112
114
|
end
|
|
113
115
|
end
|
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.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Texarkanine
|
|
@@ -63,6 +63,34 @@ dependencies:
|
|
|
63
63
|
- - "~>"
|
|
64
64
|
- !ruby/object:Gem::Version
|
|
65
65
|
version: '2.0'
|
|
66
|
+
- !ruby/object:Gem::Dependency
|
|
67
|
+
name: mutant
|
|
68
|
+
requirement: !ruby/object:Gem::Requirement
|
|
69
|
+
requirements:
|
|
70
|
+
- - "~>"
|
|
71
|
+
- !ruby/object:Gem::Version
|
|
72
|
+
version: '0.16'
|
|
73
|
+
type: :development
|
|
74
|
+
prerelease: false
|
|
75
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
76
|
+
requirements:
|
|
77
|
+
- - "~>"
|
|
78
|
+
- !ruby/object:Gem::Version
|
|
79
|
+
version: '0.16'
|
|
80
|
+
- !ruby/object:Gem::Dependency
|
|
81
|
+
name: mutant-rspec
|
|
82
|
+
requirement: !ruby/object:Gem::Requirement
|
|
83
|
+
requirements:
|
|
84
|
+
- - "~>"
|
|
85
|
+
- !ruby/object:Gem::Version
|
|
86
|
+
version: '0.16'
|
|
87
|
+
type: :development
|
|
88
|
+
prerelease: false
|
|
89
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
90
|
+
requirements:
|
|
91
|
+
- - "~>"
|
|
92
|
+
- !ruby/object:Gem::Version
|
|
93
|
+
version: '0.16'
|
|
66
94
|
- !ruby/object:Gem::Dependency
|
|
67
95
|
name: rake
|
|
68
96
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -139,28 +167,28 @@ dependencies:
|
|
|
139
167
|
requirements:
|
|
140
168
|
- - "~>"
|
|
141
169
|
- !ruby/object:Gem::Version
|
|
142
|
-
version: '0
|
|
170
|
+
version: '1.0'
|
|
143
171
|
type: :development
|
|
144
172
|
prerelease: false
|
|
145
173
|
version_requirements: !ruby/object:Gem::Requirement
|
|
146
174
|
requirements:
|
|
147
175
|
- - "~>"
|
|
148
176
|
- !ruby/object:Gem::Version
|
|
149
|
-
version: '0
|
|
177
|
+
version: '1.0'
|
|
150
178
|
- !ruby/object:Gem::Dependency
|
|
151
179
|
name: simplecov-cobertura
|
|
152
180
|
requirement: !ruby/object:Gem::Requirement
|
|
153
181
|
requirements:
|
|
154
182
|
- - "~>"
|
|
155
183
|
- !ruby/object:Gem::Version
|
|
156
|
-
version: '
|
|
184
|
+
version: '4.0'
|
|
157
185
|
type: :development
|
|
158
186
|
prerelease: false
|
|
159
187
|
version_requirements: !ruby/object:Gem::Requirement
|
|
160
188
|
requirements:
|
|
161
189
|
- - "~>"
|
|
162
190
|
- !ruby/object:Gem::Version
|
|
163
|
-
version: '
|
|
191
|
+
version: '4.0'
|
|
164
192
|
- !ruby/object:Gem::Dependency
|
|
165
193
|
name: webmock
|
|
166
194
|
requirement: !ruby/object:Gem::Requirement
|