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_CLASSES =
'alert-link'- DEFAULT_EXTERNAL_LINK_ICON =
true
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
75 76 77 |
# File 'lib/jekyll-link-decorator.rb', line 75 def self.name 'LinkDecorator' end |
Instance Method Details
#add_classes(link, classes_string) ⇒ Object
Add CSS classes to a link element
228 229 230 |
# File 'lib/jekyll-link-decorator.rb', line 228 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
251 252 253 254 255 256 257 258 259 260 |
# File 'lib/jekyll-link-decorator.rb', line 251 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 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
233 234 235 236 237 238 239 240 241 242 |
# File 'lib/jekyll-link-decorator.rb', line 233 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
137 138 139 140 141 142 143 144 145 |
# File 'lib/jekyll-link-decorator.rb', line 137 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
150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 |
# File 'lib/jekyll-link-decorator.rb', line 150 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)
212 213 214 215 216 217 218 219 220 221 222 223 224 225 |
# File 'lib/jekyll-link-decorator.rb', line 212 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_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
176 177 178 179 |
# File 'lib/jekyll-link-decorator.rb', line 176 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
126 127 128 129 130 131 132 133 |
# File 'lib/jekyll-link-decorator.rb', line 126 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
181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 |
# File 'lib/jekyll-link-decorator.rb', line 181 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”
81 82 83 84 85 86 87 88 89 90 91 92 |
# File 'lib/jekyll-link-decorator.rb', line 81 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
245 246 247 248 |
# File 'lib/jekyll-link-decorator.rb', line 245 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
99 100 101 102 103 104 |
# File 'lib/jekyll-link-decorator.rb', line 99 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
168 169 170 |
# File 'lib/jekyll-link-decorator.rb', line 168 def matches(ext) ext =~ /^\.md$/i end |
#output_ext(_ext) ⇒ Object
172 173 174 |
# File 'lib/jekyll-link-decorator.rb', line 172 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
203 204 205 206 207 208 209 |
# File 'lib/jekyll-link-decorator.rb', line 203 def plugin_enabled?(enabled) unless enabled Jekyll.logger.info 'LinkDecorator:', 'Disabled via configuration. Skipping custom link modification.' end enabled end |