jekyll-link-decorator 1.5.2 → 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: 85919ec3882d487b1545ce0508fde30b74c0fd59e35f2823a8da4b4659437004
4
- data.tar.gz: b558d2dbd36f937a5f03a670a519439d14f54625d948123977ed38ec67076af4
3
+ metadata.gz: ff4ff4aab91fea08f0f9d818a35287da8ad337181e028a2a2da62245866b3420
4
+ data.tar.gz: 81737c19fc41c5134c6741b0b6ebfb28746ca376f591c46a95a16631d56cfebf
5
5
  SHA512:
6
- metadata.gz: 5a1011beabc1ebf913123b8aff0c38c747afd70c2ee572e16aa35c0d5cebd9ae9226d92ee47fda28dd7a6ac5a6ddd77956d29906faee86b62a1bfc08977cd98e
7
- data.tar.gz: 6498a39d46641219f4939b9c3becf8b5693441c9b69ed0726d1c49cb7bfff0635df4817b13e1fe6c66befd6e5ca5bda64f500abdd681a443199bb28cbbe52408
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.5.2'
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-*`, or `link-opacity-*` 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"
70
- # alert_link_classes: "alert-link"
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'
@@ -97,8 +40,8 @@ module Jekyll
97
40
  # external-link icons for cross-domain links.
98
41
  class LinkDecorator < Jekyll::Converters::Markdown
99
42
  DEFAULT_LINK_CLASSES = 'link link-offset-3 link-offset-3-hover link-underline-primary ' \
100
- 'link-underline-opacity-0 link-underline-opacity-100-hover'
101
- DEFAULT_ALERT_LINK_CLASSES = 'alert-link'
43
+ 'link-underline-opacity-0 link-underline-opacity-100-hover focus-ring'
44
+ DEFAULT_ALERT_LINK_CLASSES = 'alert-link focus-ring'
102
45
 
103
46
  # Bootstrap 5 link-utility "families" whose defaults must not silently
104
47
  # override an author's own explicit class from the same family — see
@@ -111,9 +54,9 @@ module Jekyll
111
54
  #
112
55
  # Enumerated families (offset, both opacity families) lock to Bootstrap's
113
56
  # fixed value set, so an out-of-range value like "link-opacity-42" isn't
114
- # recognized as an override either. Color families (link-{color},
115
- # link-underline-{color}) stay generic (\S+) since Bootstrap themes can
116
- # define arbitrary custom color names.
57
+ # recognized as an override either. Color and focus-ring families
58
+ # (link-{color}, link-underline-{color}, focus-ring(-{color})?) stay
59
+ # generic (\S+) since Bootstrap themes can define arbitrary custom color names.
117
60
  LINK_CLASS_FAMILIES = [
118
61
  { # link-{color}, e.g. link-primary, link-body-emphasis (bare colored-link class;
119
62
  # excludes link-underline-*, link-offset-*, and link-opacity-* families)
@@ -131,37 +74,34 @@ module Jekyll
131
74
  { # link-opacity-{10,25,50,75,100}, each with an optional -hover variant
132
75
  # (controls link-text opacity, distinct from link-underline-opacity-*)
133
76
  pattern: /\Alink-opacity-(?:10|25|50|75|100)(-hover)?\z/
77
+ },
78
+ { # focus-ring and focus-ring-{color}, e.g. focus-ring-primary, focus-ring-success
79
+ # Bootstrap 5.3 focus-ring has no hover, offset, or opacity variants — only bare
80
+ # and color forms; color stays generic (\S+) for custom theme colors
81
+ pattern: /\Afocus-ring(-\S+)?\z/
134
82
  }
135
83
  ].freeze
136
84
  DEFAULT_EXTERNAL_LINK_ICON = true
137
85
  DEFAULT_EXTERNAL_LINK_ICON_EXCLUDED_TAGS = [].freeze
138
86
  DEFAULT_HEADING_ANCHOR = false
139
- DEFAULT_HEADING_ANCHOR_ICON = 'heading-anchor-hash pe-1'
87
+ DEFAULT_HEADING_ANCHOR_ICON = 'heading-anchor-hash'
140
88
 
141
89
  def self.name
142
90
  'LinkDecorator'
143
91
  end
144
92
 
145
- # Extract domain from a URL by removing protocol and path components
146
- # 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"
147
95
  def extract_domain(url)
148
96
  return '' if url.nil? || url.empty?
149
97
 
150
- # Remove protocol (http:// or https://)
151
98
  domain = url.sub(%r{^https?://}, '')
152
-
153
- # Extract just the domain part (before /, ?, or #)
154
99
  domain = domain.split(%r{[/?#]}).first
155
-
156
- # Convert to lowercase for case-insensitive comparison
157
100
  domain.downcase
158
101
  end
159
102
 
160
- # Check if a link is an external link pointing to a different domain
161
- # Returns true if:
162
- # - Link starts with http:// or https://
163
- # - Link domain is different from site.url domain
164
- # - 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.
165
105
  def is_external_different_domain_link?(link, site_domain)
166
106
  return false unless absolute_different_domain?(link, site_domain)
167
107
  return false if EXISTING_ICON_SELECTORS.any? { |sel| link.css(sel).any? }
@@ -169,8 +109,9 @@ module Jekyll
169
109
  true
170
110
  end
171
111
 
172
- # Returns true when href is an absolute http(s) URL pointing to a domain
173
- # 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.
174
115
  def absolute_different_domain?(link, site_domain)
175
116
  href = link['href'].to_s
176
117
  return false unless %r{^https?://}.match?(href)
@@ -182,51 +123,34 @@ module Jekyll
182
123
  end
183
124
  private :absolute_different_domain?
184
125
 
185
- # Determine if a link points to an external domain
186
- # Returns true if:
187
- # - Link is absolute (starts with http:// or https://)
188
- # - Link domain is different from site.url domain
189
- # Returns false if:
190
- # - Link is relative (starts with /, ../)
191
- # - 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.
192
128
  def cross_domain_external_link?(link, site_domain)
193
129
  href = link['href'].to_s
194
-
195
- # Relative URLs (starting with / or ../) are internal links
196
130
  return false if %r{^(/|\.\.)}.match?(href)
197
131
 
198
132
  absolute_different_domain?(link, site_domain)
199
133
  end
200
134
 
201
- # Add target="_blank" to external cross-domain links
202
- # Only adds if target is not already set
135
+ # Sets target="_blank" on a cross-domain link, unless it already has a target.
203
136
  def add_external_target_blank(link, site_domain)
204
- # Skip if link already has a target attribute
205
137
  return if link['target'].to_s.strip != ''
206
-
207
- # Add target="_blank" if this is a cross-domain external link
208
138
  return unless cross_domain_external_link?(link, site_domain)
209
139
 
210
140
  link['target'] = '_blank'
211
141
  end
212
142
 
213
- # Add rel="noopener noreferrer" to target="_blank" links, preserving
214
- # any existing rel attribute values
143
+ # Adds rel="noopener noreferrer" to a target="_blank" link, keeping any
144
+ # rel values the author already set.
215
145
  def add_external_target_protection(link)
216
146
  target = link['target'].to_s
217
-
218
- # Only add protection for target="_blank"
219
147
  return unless target == '_blank'
220
148
 
221
- # Get existing rel attribute (if any)
222
149
  existing_rel = link['rel'].to_s.strip
223
150
  rel_values = existing_rel.empty? ? [] : existing_rel.split(/\s+/)
224
-
225
- # Add required security values if not already present
226
151
  rel_values << 'noopener' unless rel_values.include?('noopener')
227
152
  rel_values << 'noreferrer' unless rel_values.include?('noreferrer')
228
153
 
229
- # Set the updated rel attribute
230
154
  link['rel'] = rel_values.join(' ')
231
155
  end
232
156
 
@@ -264,8 +188,8 @@ module Jekyll
264
188
  html
265
189
  end
266
190
 
267
- # Check if the plugin is enabled in configuration
268
- # 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.
269
193
  def plugin_enabled?(enabled)
270
194
  unless enabled
271
195
  Jekyll.logger.info 'LinkDecorator:',
@@ -274,17 +198,16 @@ module Jekyll
274
198
  enabled
275
199
  end
276
200
 
277
- # 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.
278
203
  def apply_link_styles(doc, config)
279
204
  default_classes = config.fetch('default_link_classes', DEFAULT_LINK_CLASSES)
280
205
  alert_classes = config.fetch('alert_link_classes', DEFAULT_ALERT_LINK_CLASSES)
281
206
 
282
- # Apply alert classes to links inside p.alert or blockquote.alert
283
207
  doc.css('p.alert a:not(.btn), blockquote.alert a:not(.btn)').each do |link|
284
208
  add_classes(link, alert_classes)
285
209
  end
286
210
 
287
- # Apply default classes to all other non-alert links
288
211
  doc.css('a:not(.btn)').each do |link|
289
212
  next if link.ancestors('p.alert, blockquote.alert').any?
290
213
 
@@ -305,12 +228,12 @@ module Jekyll
305
228
  end
306
229
  end
307
230
 
308
- # Add CSS classes to a link element
309
231
  def add_classes(link, classes_string)
310
232
  classes_string.split.each { |cls| link.add_class(cls) }
311
233
  end
312
234
 
313
- # 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.
314
237
  def add_external_link_features(doc, config)
315
238
  site_domain = extract_site_domain
316
239
  return if site_domain.empty?
@@ -322,14 +245,14 @@ module Jekyll
322
245
  end
323
246
  end
324
247
 
325
- # Extract site domain from Jekyll configuration
326
248
  def extract_site_domain
327
249
  site_url = @config['url'] || @config['baseurl'] || ''
328
250
  extract_domain(site_url)
329
251
  end
330
252
 
331
- # Inject a Font Awesome anchor link inside each heading (h1h6).
332
- # 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.
333
256
  def add_heading_anchors(doc)
334
257
  enabled = @config.key?('with_heading_anchor') ? @config['with_heading_anchor'] : DEFAULT_HEADING_ANCHOR
335
258
  return unless enabled
@@ -346,7 +269,7 @@ module Jekyll
346
269
  id = resolve_heading_id(heading, id_counts)
347
270
 
348
271
  anchor = Nokogiri::XML::Node.new('a', doc)
349
- anchor['class'] = 'heading-anchor mx-1'
272
+ anchor['class'] = 'heading-anchor p-1 mx-1 focus-ring'
350
273
  anchor['href'] = "##{id}"
351
274
  anchor['data-copy-anchor'] = "##{id}"
352
275
  anchor['aria-label'] = "Link to #{heading.text.strip}"
@@ -360,8 +283,9 @@ module Jekyll
360
283
  end
361
284
  end
362
285
 
363
- # Return the heading's existing id, or generate and assign one from its text.
364
- # 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.
365
289
  def resolve_heading_id(heading, id_counts)
366
290
  existing = heading['id'].to_s.strip
367
291
  return existing unless existing.empty?
@@ -374,8 +298,10 @@ module Jekyll
374
298
  final_id
375
299
  end
376
300
 
377
- # Convert heading text to a URL-safe slug matching the link.html.liquid algorithm:
378
- # 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.
379
305
  def slugify(text)
380
306
  text.strip
381
307
  .downcase
@@ -387,7 +313,9 @@ module Jekyll
387
313
 
388
314
  private :add_heading_anchors, :resolve_heading_id, :slugify
389
315
 
390
- # 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.
391
319
  def add_external_icon(link, site_domain, config, doc)
392
320
  external_link_icon = config.fetch('external_link_icon', DEFAULT_EXTERNAL_LINK_ICON)
393
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.5.2
4
+ version: 1.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alain Reguera Delgado