jekyll-link-decorator 1.6.0 → 1.7.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: 4d91801b17b914f5fd1b053dabb0ca88e6d814c80dd0a58b929272ddaf636224
4
- data.tar.gz: 22127cd8018119d37a82855bed5f73476783f46610b763c4eb31a5095689cdac
3
+ metadata.gz: ff4ff4aab91fea08f0f9d818a35287da8ad337181e028a2a2da62245866b3420
4
+ data.tar.gz: 81737c19fc41c5134c6741b0b6ebfb28746ca376f591c46a95a16631d56cfebf
5
5
  SHA512:
6
- metadata.gz: 22225853f9368b78e14d32a8c44d945acc6c526a225992603d51317892c2c5ae9f958bf6f4882352441087a00d3f442071142f38f7953f2189bb4c1fe9a0daa1
7
- data.tar.gz: f83df55c641942409368e632ef79763a669d06d558fe02b2911a4198361d6c81f936360213ce9858c48d32ebd64f465abdca96d42e7bcadc6c6a0613c0cbf002
6
+ metadata.gz: e912b0fbdc82c9640912f96b5a4f436d378e79fd4f9099eaba218759d7dc6ca688285b6785aa34163d89c749e153b9e5de37623277e66c16f892abe23fe3be02
7
+ data.tar.gz: 0ba21a73e60ad550d74415a22f06b2ada6bf83d8bef37521e778fcb546ca39bdb0695bdeefe1fb53c6fe65003608c2725f8446f3943f69664e105c4f0d704880
@@ -6,6 +6,6 @@ module Jekyll
6
6
  # Regenerated alongside the gemspec on every `make gemspec` run (locally
7
7
  # or in CI), so it always matches spec.version in the built gem. Any
8
8
  # hand edit is overwritten on the next run.
9
- VERSION = '1.6.0'
9
+ VERSION = '1.7.0'
10
10
  end
11
11
  end
@@ -1,82 +1,25 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # This Jekyll plugin, `link_decorator`, enhances `<a>` tags in Markdown content
4
- # with Bootstrap CSS classes and external-link icons. It applies different
5
- # classes depending on whether a link sits inside a Bootstrap alert box or
6
- # stands alone, and skips elements with the `.btn` class.
3
+ # This plugin decorates <a> tags in Markdown content with Bootstrap CSS
4
+ # classes, security attributes, and Font Awesome icons. Links inside a
5
+ # Bootstrap alert box get alert-specific classes; other links get the
6
+ # default classes; links carrying a `.btn` class are left untouched.
7
7
  #
8
- # === External Link Icon Support ===
8
+ # For links to a different domain than the site's own (site.url), it adds
9
+ # target="_blank", the rel="noopener noreferrer" security attributes, and an
10
+ # external-link icon at the end of the link text.
9
11
  #
10
- # For links pointing to external domains (different from site.url), the plugin
11
- # adds a Font Awesome external-link icon (fa-solid fa-external-link) at the
12
- # end of the link text.
12
+ # It can also inject a permalink icon into each heading (h1-h6) so every
13
+ # section gets a shareable URL.
13
14
  #
14
- # === External Link Detection & Target Management ===
15
+ # Turn these on and customize them from `_config.yml` with the
16
+ # `with_link_decorator`, `with_link_decorator_data`, `with_heading_anchor`,
17
+ # and `with_heading_anchor_data` keys. See docs/configuration.md for the
18
+ # full key reference, defaults, and worked `_config.yml` examples.
15
19
  #
16
- # The plugin detects external links and manages their behavior:
17
- # - Relative URLs (starting with /, ../) and same-domain absolute URLs open in the same window
18
- # - Absolute URLs pointing to different domains (http:// or https://) get target="_blank"
19
- # - Links with target="_blank" receive rel="noopener noreferrer" protection against
20
- # security vulnerabilities (window.opener access, referrer leakage)
21
- #
22
- # === Configuration ===
23
- #
24
- # Customize the plugin's classes by adding `with_link_decorator` and
25
- # `with_link_decorator_data` to your `_config.yml`.
26
- #
27
- # * `with_link_decorator`: Boolean (default: true) that controls whether the plugin runs.
28
- # Set to false to disable automatic link styling and icon insertion.
29
- #
30
- # * `with_link_decorator_data`: A dictionary containing optional configuration keys:
31
- # - `default_link_classes`: Classes applied to links outside an .alert.
32
- # Example: `default_link_classes: "text-decoration-none"`
33
- # If a link already carries its own `link-{color}`, `link-underline-*`,
34
- # `link-offset-*`, `link-underline-opacity-*`, `link-opacity-*`, or `focus-ring(-{color})?` class,
35
- # the plugin skips the matching default token automatically — see LINK_CLASS_FAMILIES.
36
- # - `alert_link_classes`: Classes applied to links inside an .alert box.
37
- # Example: `alert_link_classes: "text-decoration-underline"`
38
- # - `external_link_icon`: Boolean (default: true) — show the icon on cross-domain
39
- # external links. Set to false to disable.
40
- # - `external_link_icon_excluded_tags`: List of HTML child tag names (default: []) whose presence
41
- # inside an anchor suppresses the external-link icon. Security attributes
42
- # (target="_blank", rel="noopener noreferrer") still apply.
43
- # Example: `external_link_icon_excluded_tags: ["img", "svg"]`
44
- #
45
- # === Heading Anchor Support ===
46
- #
47
- # The plugin can inject a permalink icon inside each heading (h1–h6) so every
48
- # section gets a shareable URL. Configure it via two top-level keys (separate
49
- # from `with_link_decorator_data`):
50
- #
51
- # * `with_heading_anchor`: Boolean (default: false) — set to true to inject heading anchors.
52
- #
53
- # * `with_heading_anchor_data`: A dictionary containing optional configuration keys:
54
- # - `icon`: CSS classes applied to the anchor's glyph element (default:
55
- # "heading-anchor-hash pe-1", which renders a plain "#" via the
56
- # jekyll-theme-centos-base theme's CSS). Accepts any class list, including
57
- # Font Awesome icon classes (e.g. "fa-solid fa-hashtag") for sites that
58
- # want an icon-font glyph instead.
59
- # - `icon_size`: Optional size modifier class appended after the icon classes.
60
- #
61
- # Without configuration, the plugin uses its default classes and enables
62
- # external-link icons.
63
- #
64
- # Example `_config.yml`:
65
- #
66
- # ```yaml
67
- # with_link_decorator: true
68
- # with_link_decorator_data:
69
- # default_link_classes: "link link-offset-3 link-offset-3-hover link-underline-primary link-underline-opacity-0 link-underline-opacity-100-hover focus-ring"
70
- # alert_link_classes: "alert-link focus-ring"
71
- # external_link_icon: true
72
- # external_link_icon_excluded_tags:
73
- # - img
74
- # - svg
75
- #
76
- # with_heading_anchor: true
77
- # with_heading_anchor_data:
78
- # icon: "heading-anchor-hash pe-1"
79
- # ```
20
+ # A link that already carries its own Bootstrap link-utility class (see
21
+ # LINK_CLASS_FAMILIES below) keeps that class instead of getting the
22
+ # plugin's default for the same family.
80
23
 
81
24
  require 'nokogiri'
82
25
  require_relative 'jekyll-link-decorator/version'
@@ -141,32 +84,24 @@ module Jekyll
141
84
  DEFAULT_EXTERNAL_LINK_ICON = true
142
85
  DEFAULT_EXTERNAL_LINK_ICON_EXCLUDED_TAGS = [].freeze
143
86
  DEFAULT_HEADING_ANCHOR = false
144
- DEFAULT_HEADING_ANCHOR_ICON = 'heading-anchor-hash pe-1'
87
+ DEFAULT_HEADING_ANCHOR_ICON = 'heading-anchor-hash'
145
88
 
146
89
  def self.name
147
90
  'LinkDecorator'
148
91
  end
149
92
 
150
- # Extract domain from a URL by removing protocol and path components
151
- # Example: "https://example.com/path?query=1#anchor" -> "example.com"
93
+ # Returns a URL's domain, stripped of scheme and path.
94
+ # "https://example.com/path?query=1#anchor" -> "example.com"
152
95
  def extract_domain(url)
153
96
  return '' if url.nil? || url.empty?
154
97
 
155
- # Remove protocol (http:// or https://)
156
98
  domain = url.sub(%r{^https?://}, '')
157
-
158
- # Extract just the domain part (before /, ?, or #)
159
99
  domain = domain.split(%r{[/?#]}).first
160
-
161
- # Convert to lowercase for case-insensitive comparison
162
100
  domain.downcase
163
101
  end
164
102
 
165
- # Check if a link is an external link pointing to a different domain
166
- # Returns true if:
167
- # - Link starts with http:// or https://
168
- # - Link domain is different from site.url domain
169
- # - Link doesn't already have an external-link icon child
103
+ # True when this link needs the external-link icon: an absolute,
104
+ # cross-domain link that doesn't already have one.
170
105
  def is_external_different_domain_link?(link, site_domain)
171
106
  return false unless absolute_different_domain?(link, site_domain)
172
107
  return false if EXISTING_ICON_SELECTORS.any? { |sel| link.css(sel).any? }
@@ -174,8 +109,9 @@ module Jekyll
174
109
  true
175
110
  end
176
111
 
177
- # Returns true when href is an absolute http(s) URL pointing to a domain
178
- # different from site_domain. Shared guard used by both domain-check methods.
112
+ # True when href is an absolute http(s) URL on a domain other than
113
+ # site_domain. Shared by is_external_different_domain_link? and
114
+ # cross_domain_external_link? below.
179
115
  def absolute_different_domain?(link, site_domain)
180
116
  href = link['href'].to_s
181
117
  return false unless %r{^https?://}.match?(href)
@@ -187,51 +123,34 @@ module Jekyll
187
123
  end
188
124
  private :absolute_different_domain?
189
125
 
190
- # Determine if a link points to an external domain
191
- # Returns true if:
192
- # - Link is absolute (starts with http:// or https://)
193
- # - Link domain is different from site.url domain
194
- # Returns false if:
195
- # - Link is relative (starts with /, ../)
196
- # - Link domain matches site.url domain
126
+ # True when this link needs target="_blank": absolute and cross-domain.
127
+ # Relative URLs (/, ../) are always treated as internal.
197
128
  def cross_domain_external_link?(link, site_domain)
198
129
  href = link['href'].to_s
199
-
200
- # Relative URLs (starting with / or ../) are internal links
201
130
  return false if %r{^(/|\.\.)}.match?(href)
202
131
 
203
132
  absolute_different_domain?(link, site_domain)
204
133
  end
205
134
 
206
- # Add target="_blank" to external cross-domain links
207
- # Only adds if target is not already set
135
+ # Sets target="_blank" on a cross-domain link, unless it already has a target.
208
136
  def add_external_target_blank(link, site_domain)
209
- # Skip if link already has a target attribute
210
137
  return if link['target'].to_s.strip != ''
211
-
212
- # Add target="_blank" if this is a cross-domain external link
213
138
  return unless cross_domain_external_link?(link, site_domain)
214
139
 
215
140
  link['target'] = '_blank'
216
141
  end
217
142
 
218
- # Add rel="noopener noreferrer" to target="_blank" links, preserving
219
- # any existing rel attribute values
143
+ # Adds rel="noopener noreferrer" to a target="_blank" link, keeping any
144
+ # rel values the author already set.
220
145
  def add_external_target_protection(link)
221
146
  target = link['target'].to_s
222
-
223
- # Only add protection for target="_blank"
224
147
  return unless target == '_blank'
225
148
 
226
- # Get existing rel attribute (if any)
227
149
  existing_rel = link['rel'].to_s.strip
228
150
  rel_values = existing_rel.empty? ? [] : existing_rel.split(/\s+/)
229
-
230
- # Add required security values if not already present
231
151
  rel_values << 'noopener' unless rel_values.include?('noopener')
232
152
  rel_values << 'noreferrer' unless rel_values.include?('noreferrer')
233
153
 
234
- # Set the updated rel attribute
235
154
  link['rel'] = rel_values.join(' ')
236
155
  end
237
156
 
@@ -269,8 +188,8 @@ module Jekyll
269
188
  html
270
189
  end
271
190
 
272
- # Check if the plugin is enabled in configuration
273
- # Defaults to true if not explicitly configured
191
+ # Logs a notice when disabled; returns the given enabled flag unchanged.
192
+ # The caller (decorate_html) decides what "enabled" defaults to.
274
193
  def plugin_enabled?(enabled)
275
194
  unless enabled
276
195
  Jekyll.logger.info 'LinkDecorator:',
@@ -279,17 +198,16 @@ module Jekyll
279
198
  enabled
280
199
  end
281
200
 
282
- # Apply CSS classes to links based on their context (alert or default)
201
+ # Applies alert_classes to links inside an .alert box, default_classes
202
+ # to every other link.
283
203
  def apply_link_styles(doc, config)
284
204
  default_classes = config.fetch('default_link_classes', DEFAULT_LINK_CLASSES)
285
205
  alert_classes = config.fetch('alert_link_classes', DEFAULT_ALERT_LINK_CLASSES)
286
206
 
287
- # Apply alert classes to links inside p.alert or blockquote.alert
288
207
  doc.css('p.alert a:not(.btn), blockquote.alert a:not(.btn)').each do |link|
289
208
  add_classes(link, alert_classes)
290
209
  end
291
210
 
292
- # Apply default classes to all other non-alert links
293
211
  doc.css('a:not(.btn)').each do |link|
294
212
  next if link.ancestors('p.alert, blockquote.alert').any?
295
213
 
@@ -310,12 +228,12 @@ module Jekyll
310
228
  end
311
229
  end
312
230
 
313
- # Add CSS classes to a link element
314
231
  def add_classes(link, classes_string)
315
232
  classes_string.split.each { |cls| link.add_class(cls) }
316
233
  end
317
234
 
318
- # Add external link features: target="_blank", icons, and security attributes
235
+ # Adds target="_blank", the external-link icon, and rel security
236
+ # attributes to every link on the page, in that order.
319
237
  def add_external_link_features(doc, config)
320
238
  site_domain = extract_site_domain
321
239
  return if site_domain.empty?
@@ -327,14 +245,14 @@ module Jekyll
327
245
  end
328
246
  end
329
247
 
330
- # Extract site domain from Jekyll configuration
331
248
  def extract_site_domain
332
249
  site_url = @config['url'] || @config['baseurl'] || ''
333
250
  extract_domain(site_url)
334
251
  end
335
252
 
336
- # Inject a Font Awesome anchor link inside each heading (h1h6).
337
- # Reads with_heading_anchor and with_heading_anchor_data from @config directly.
253
+ # Injects a permalink anchor into each heading (h1-h6). The anchor's
254
+ # glyph defaults to a plain "#"; with_heading_anchor_data can swap it
255
+ # for a Font Awesome icon or any other class list.
338
256
  def add_heading_anchors(doc)
339
257
  enabled = @config.key?('with_heading_anchor') ? @config['with_heading_anchor'] : DEFAULT_HEADING_ANCHOR
340
258
  return unless enabled
@@ -351,7 +269,7 @@ module Jekyll
351
269
  id = resolve_heading_id(heading, id_counts)
352
270
 
353
271
  anchor = Nokogiri::XML::Node.new('a', doc)
354
- anchor['class'] = 'heading-anchor mx-1'
272
+ anchor['class'] = 'heading-anchor p-1 mx-1 focus-ring'
355
273
  anchor['href'] = "##{id}"
356
274
  anchor['data-copy-anchor'] = "##{id}"
357
275
  anchor['aria-label'] = "Link to #{heading.text.strip}"
@@ -365,8 +283,9 @@ module Jekyll
365
283
  end
366
284
  end
367
285
 
368
- # Return the heading's existing id, or generate and assign one from its text.
369
- # id_counts tracks generated slugs to produce unique suffixes for duplicates.
286
+ # Returns the heading's existing id, or generates one from its text and
287
+ # assigns it. id_counts tracks slugs already used, so repeated headings
288
+ # get unique "-1", "-2", ... suffixes.
370
289
  def resolve_heading_id(heading, id_counts)
371
290
  existing = heading['id'].to_s.strip
372
291
  return existing unless existing.empty?
@@ -379,8 +298,10 @@ module Jekyll
379
298
  final_id
380
299
  end
381
300
 
382
- # Convert heading text to a URL-safe slug matching the link.html.liquid algorithm:
383
- # strip downcase → spaces to hyphens → remove ", ', ?, &, #, / → collapse hyphens
301
+ # Converts heading text to a URL-safe slug, loosely mirroring the slug
302
+ # rules in jekyll-theme-centos-base's link.html.liquid (same characters
303
+ # stripped and replaced), plus collapsing repeated/edge hyphens so
304
+ # headings with unusual punctuation still get a clean id.
384
305
  def slugify(text)
385
306
  text.strip
386
307
  .downcase
@@ -392,7 +313,9 @@ module Jekyll
392
313
 
393
314
  private :add_heading_anchors, :resolve_heading_id, :slugify
394
315
 
395
- # Add external-link icon to cross-domain links
316
+ # Appends the external-link icon to a cross-domain link, unless the
317
+ # icon is disabled, the link already has an icon-bearing tag excluded
318
+ # via external_link_icon_excluded_tags, or one is already present.
396
319
  def add_external_icon(link, site_domain, config, doc)
397
320
  external_link_icon = config.fetch('external_link_icon', DEFAULT_EXTERNAL_LINK_ICON)
398
321
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jekyll-link-decorator
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.6.0
4
+ version: 1.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alain Reguera Delgado