Class: Jekyll::Converters::LinkDecorator
- Inherits:
-
Markdown
- Object
- Markdown
- Jekyll::Converters::LinkDecorator
- Defined in:
- lib/jekyll-link-decorator.rb
Overview
This custom converter applies specific CSS classes to <a> tags based on their context (e.g., inside an alert box) and excludes buttons, all done via Nokogiri’s powerful CSS selectors. It also adds external-link icons for cross-domain external links.
Constant Summary collapse
- DEFAULT_LINK_CLASSES =
'link link-offset-3 link-offset-3-hover link-underline-primary ' \ 'link-underline-opacity-0 link-underline-opacity-100-hover'
- DEFAULT_ALERT_LINK_CLASSES =
'alert-link'- DEFAULT_EXTERNAL_LINK_ICON =
true- DEFAULT_EXTERNAL_LINK_ICON_EXCLUDED_TAGS =
[].freeze
Class Method Summary collapse
Instance Method Summary collapse
-
#add_classes(link, classes_string) ⇒ Object
Add CSS classes to a link element.
-
#add_external_icon(link, site_domain, config, doc) ⇒ Object
Add external-link icon to cross-domain external links.
-
#add_external_link_features(doc, config) ⇒ Object
Add external link features: target=“_blank”, icons, and security attributes.
-
#add_external_target_blank(link, site_domain) ⇒ Object
Add target=“_blank” to external cross-domain links Only adds if target is not already set.
-
#add_external_target_protection(link) ⇒ Object
Add security attributes to links with target=“_blank” Adds rel=“noopener noreferrer” to prevent security vulnerabilities Preserves any existing rel attribute values.
-
#apply_link_styles(doc, config) ⇒ Object
Apply CSS classes to links based on their context (alert or default).
- #convert(content) ⇒ Object
-
#cross_domain_external_link?(link, site_domain) ⇒ Boolean
Determine if a link points to an external domain Returns true if: - Link is absolute (starts with http:// or https://) - Link domain is different from site.url domain Returns false if: - Link is relative (starts with /, ../) - Link domain matches site.url domain.
- #decorate_html(html) ⇒ Object
-
#extract_domain(url) ⇒ Object
Extract domain from a URL by removing protocol and path components Example: “example.com/path?query=1#anchor” -> “example.com”.
-
#extract_site_domain ⇒ Object
Extract site domain from Jekyll configuration.
-
#is_external_different_domain_link?(link, site_domain) ⇒ Boolean
Check if a link is an external link pointing to a different domain Returns true if: - Link starts with http:// or https:// - Link domain is different from site.url domain - Link doesn’t already have an external-link icon child.
- #matches(ext) ⇒ Object
- #output_ext(_ext) ⇒ Object
-
#plugin_enabled?(enabled) ⇒ Boolean
Check if the plugin is enabled in configuration Defaults to true if not explicitly configured.
Class Method Details
.name ⇒ Object
83 84 85 |
# File 'lib/jekyll-link-decorator.rb', line 83 def self.name 'LinkDecorator' end |
Instance Method Details
#add_classes(link, classes_string) ⇒ Object
Add CSS classes to a link element
236 237 238 |
# File 'lib/jekyll-link-decorator.rb', line 236 def add_classes(link, classes_string) classes_string.split.each { |cls| link.add_class(cls) } end |
#add_external_icon(link, site_domain, config, doc) ⇒ Object
Add external-link icon to cross-domain external links
259 260 261 262 263 264 265 266 267 268 269 270 271 272 |
# File 'lib/jekyll-link-decorator.rb', line 259 def add_external_icon(link, site_domain, config, doc) external_link_icon = config.fetch('external_link_icon', DEFAULT_EXTERNAL_LINK_ICON) return unless external_link_icon = config.fetch('external_link_icon_excluded_tags', DEFAULT_EXTERNAL_LINK_ICON_EXCLUDED_TAGS) return if .any? { |tag| link.css(tag).any? } return unless is_external_different_domain_link?(link, site_domain) icon = Nokogiri::XML::Node.new('i', doc) icon['class'] = 'fa-solid fa-external-link ms-1' link.add_child(icon) end |
#add_external_link_features(doc, config) ⇒ Object
Add external link features: target=“_blank”, icons, and security attributes
241 242 243 244 245 246 247 248 249 250 |
# File 'lib/jekyll-link-decorator.rb', line 241 def add_external_link_features(doc, config) site_domain = extract_site_domain return if site_domain.empty? doc.css('a:not(.btn)').each do |link| add_external_target_blank(link, site_domain) add_external_icon(link, site_domain, config, doc) add_external_target_protection(link) end end |
#add_external_target_blank(link, site_domain) ⇒ Object
Add target=“_blank” to external cross-domain links Only adds if target is not already set
145 146 147 148 149 150 151 152 153 |
# File 'lib/jekyll-link-decorator.rb', line 145 def add_external_target_blank(link, site_domain) # Skip if link already has a target attribute return if link['target'].to_s.strip != '' # Add target="_blank" if this is a cross-domain external link return unless cross_domain_external_link?(link, site_domain) link['target'] = '_blank' end |
#add_external_target_protection(link) ⇒ Object
Add security attributes to links with target=“_blank” Adds rel=“noopener noreferrer” to prevent security vulnerabilities Preserves any existing rel attribute values
158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 |
# File 'lib/jekyll-link-decorator.rb', line 158 def add_external_target_protection(link) target = link['target'].to_s # Only add protection for target="_blank" return unless target == '_blank' # Get existing rel attribute (if any) existing_rel = link['rel'].to_s.strip rel_values = existing_rel.empty? ? [] : existing_rel.split(/\s+/) # Add required security values if not already present rel_values << 'noopener' unless rel_values.include?('noopener') rel_values << 'noreferrer' unless rel_values.include?('noreferrer') # Set the updated rel attribute link['rel'] = rel_values.join(' ') end |
#apply_link_styles(doc, config) ⇒ Object
Apply CSS classes to links based on their context (alert or default)
220 221 222 223 224 225 226 227 228 229 230 231 232 233 |
# File 'lib/jekyll-link-decorator.rb', line 220 def apply_link_styles(doc, config) default_classes = config.fetch('default_link_classes', DEFAULT_LINK_CLASSES) alert_classes = config.fetch('alert_link_classes', DEFAULT_ALERT_LINK_CLASSES) # Apply alert classes to links inside p.alert doc.css('p.alert a:not(.btn)').each do |link| add_classes(link, alert_classes) end # Apply default classes to all other non-alert links doc.css('a:not(.btn)').each do |link| add_classes(link, default_classes) unless link.ancestors('p.alert').any? end end |
#convert(content) ⇒ Object
184 185 186 187 |
# File 'lib/jekyll-link-decorator.rb', line 184 def convert(content) html = super decorate_html(html) end |
#cross_domain_external_link?(link, site_domain) ⇒ Boolean
Determine if a link points to an external domain Returns true if:
-
Link is absolute (starts with http:// or https://)
-
Link domain is different from site.url domain
Returns false if:
-
Link is relative (starts with /, ../)
-
Link domain matches site.url domain
134 135 136 137 138 139 140 141 |
# File 'lib/jekyll-link-decorator.rb', line 134 def cross_domain_external_link?(link, site_domain) href = link['href'].to_s # Relative URLs (starting with / or ../) are internal links return false if %r{^(/|\.\.)}.match?(href) absolute_different_domain?(link, site_domain) end |
#decorate_html(html) ⇒ Object
189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 |
# File 'lib/jekyll-link-decorator.rb', line 189 def decorate_html(html) enabled = @config.key?('with_link_decorator') ? @config['with_link_decorator'] : true config = @config['with_link_decorator_data'] || {} return html unless plugin_enabled?(enabled) doc = Nokogiri::HTML::DocumentFragment.parse(html) apply_link_styles(doc, config) add_external_link_features(doc, config) doc.to_html rescue StandardError => e Jekyll.logger.error 'LinkDecorator Error:', "An error occurred during link decoration: #{e.}" Jekyll.logger.error 'Backtrace:', e.backtrace.join("\n") html end |
#extract_domain(url) ⇒ Object
Extract domain from a URL by removing protocol and path components Example: “example.com/path?query=1#anchor” -> “example.com”
89 90 91 92 93 94 95 96 97 98 99 100 |
# File 'lib/jekyll-link-decorator.rb', line 89 def extract_domain(url) return '' if url.nil? || url.empty? # Remove protocol (http:// or https://) domain = url.sub(%r{^https?://}, '') # Extract just the domain part (before /, ?, or #) domain = domain.split(%r{[/?#]}).first # Convert to lowercase for case-insensitive comparison domain.downcase end |
#extract_site_domain ⇒ Object
Extract site domain from Jekyll configuration
253 254 255 256 |
# File 'lib/jekyll-link-decorator.rb', line 253 def extract_site_domain site_url = @config['url'] || @config['baseurl'] || '' extract_domain(site_url) end |
#is_external_different_domain_link?(link, site_domain) ⇒ Boolean
Check if a link is an external link pointing to a different domain Returns true if:
-
Link starts with http:// or https://
-
Link domain is different from site.url domain
-
Link doesn’t already have an external-link icon child
107 108 109 110 111 112 |
# File 'lib/jekyll-link-decorator.rb', line 107 def is_external_different_domain_link?(link, site_domain) return false unless absolute_different_domain?(link, site_domain) return false if EXISTING_ICON_SELECTORS.any? { |sel| link.css(sel).any? } true end |
#matches(ext) ⇒ Object
176 177 178 |
# File 'lib/jekyll-link-decorator.rb', line 176 def matches(ext) ext =~ /^\.md$/i end |
#output_ext(_ext) ⇒ Object
180 181 182 |
# File 'lib/jekyll-link-decorator.rb', line 180 def output_ext(_ext) '.html' end |
#plugin_enabled?(enabled) ⇒ Boolean
Check if the plugin is enabled in configuration Defaults to true if not explicitly configured
211 212 213 214 215 216 217 |
# File 'lib/jekyll-link-decorator.rb', line 211 def plugin_enabled?(enabled) unless enabled Jekyll.logger.info 'LinkDecorator:', 'Disabled via configuration. Skipping custom link modification.' end enabled end |