jekyll-link-decorator 1.6.0 → 1.8.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: 0a4872962803af2990a40727305d6c2addb9ea7d3df92d614d00371a0aaf77ad
4
+ data.tar.gz: 47250445c0c9163963082deb6ee986022dc44e4db2ecd79d2b8367c1ecc3000b
5
5
  SHA512:
6
- metadata.gz: 22225853f9368b78e14d32a8c44d945acc6c526a225992603d51317892c2c5ae9f958bf6f4882352441087a00d3f442071142f38f7953f2189bb4c1fe9a0daa1
7
- data.tar.gz: f83df55c641942409368e632ef79763a669d06d558fe02b2911a4198361d6c81f936360213ce9858c48d32ebd64f465abdca96d42e7bcadc6c6a0613c0cbf002
6
+ metadata.gz: b8328e80c3b9ba63802c5aef9133054731c19112e078ced348ff5f30fa8da47c6c02531d4bb140bd016ae7e9c376921a00561df16eaa2695a3f9cb000496d5b5
7
+ data.tar.gz: ef76d4e238557f5ad72114c1701519f1323af626149f8730bc873f9d6213f3a8c5624afd29357dc021aa5c507db29a80d7f56e3d0b93e29f114b8431accd9b0a
@@ -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.8.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'
@@ -96,7 +39,7 @@ module Jekyll
96
39
  # box) via Nokogiri CSS selectors, excluding buttons. Also adds
97
40
  # external-link icons for cross-domain links.
98
41
  class LinkDecorator < Jekyll::Converters::Markdown
99
- DEFAULT_LINK_CLASSES = 'link link-offset-3 link-offset-3-hover link-underline-primary ' \
42
+ DEFAULT_LINK_CLASSES = 'link link-offset-2 link-offset-2-hover link-underline ' \
100
43
  'link-underline-opacity-0 link-underline-opacity-100-hover focus-ring'
101
44
  DEFAULT_ALERT_LINK_CLASSES = 'alert-link focus-ring'
102
45
 
@@ -119,8 +62,9 @@ module Jekyll
119
62
  # excludes link-underline-*, link-offset-*, and link-opacity-* families)
120
63
  pattern: /\Alink-(?!underline\b|offset-|opacity-)\S+\z/
121
64
  },
122
- { # link-underline-{color}, e.g. link-underline-success (excludes -opacity-*)
123
- pattern: /\Alink-underline-(?!opacity)\S+\z/
65
+ { # link-underline and link-underline-{color}, e.g. link-underline-success
66
+ # (excludes -opacity-*); the bare class takes the link's own text color
67
+ pattern: /\Alink-underline(?:-(?!opacity)\S+)?\z/
124
68
  },
125
69
  { # link-offset-{1,2,3}, each with an optional -hover variant
126
70
  pattern: /\Alink-offset-[123](-hover)?\z/
@@ -141,32 +85,24 @@ module Jekyll
141
85
  DEFAULT_EXTERNAL_LINK_ICON = true
142
86
  DEFAULT_EXTERNAL_LINK_ICON_EXCLUDED_TAGS = [].freeze
143
87
  DEFAULT_HEADING_ANCHOR = false
144
- DEFAULT_HEADING_ANCHOR_ICON = 'heading-anchor-hash pe-1'
88
+ DEFAULT_HEADING_ANCHOR_ICON = 'heading-anchor-hash'
145
89
 
146
90
  def self.name
147
91
  'LinkDecorator'
148
92
  end
149
93
 
150
- # Extract domain from a URL by removing protocol and path components
151
- # Example: "https://example.com/path?query=1#anchor" -> "example.com"
94
+ # Returns a URL's domain, stripped of scheme and path.
95
+ # "https://example.com/path?query=1#anchor" -> "example.com"
152
96
  def extract_domain(url)
153
97
  return '' if url.nil? || url.empty?
154
98
 
155
- # Remove protocol (http:// or https://)
156
99
  domain = url.sub(%r{^https?://}, '')
157
-
158
- # Extract just the domain part (before /, ?, or #)
159
100
  domain = domain.split(%r{[/?#]}).first
160
-
161
- # Convert to lowercase for case-insensitive comparison
162
101
  domain.downcase
163
102
  end
164
103
 
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
104
+ # True when this link needs the external-link icon: an absolute,
105
+ # cross-domain link that doesn't already have one.
170
106
  def is_external_different_domain_link?(link, site_domain)
171
107
  return false unless absolute_different_domain?(link, site_domain)
172
108
  return false if EXISTING_ICON_SELECTORS.any? { |sel| link.css(sel).any? }
@@ -174,8 +110,9 @@ module Jekyll
174
110
  true
175
111
  end
176
112
 
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.
113
+ # True when href is an absolute http(s) URL on a domain other than
114
+ # site_domain. Shared by is_external_different_domain_link? and
115
+ # cross_domain_external_link? below.
179
116
  def absolute_different_domain?(link, site_domain)
180
117
  href = link['href'].to_s
181
118
  return false unless %r{^https?://}.match?(href)
@@ -187,51 +124,34 @@ module Jekyll
187
124
  end
188
125
  private :absolute_different_domain?
189
126
 
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
127
+ # True when this link needs target="_blank": absolute and cross-domain.
128
+ # Relative URLs (/, ../) are always treated as internal.
197
129
  def cross_domain_external_link?(link, site_domain)
198
130
  href = link['href'].to_s
199
-
200
- # Relative URLs (starting with / or ../) are internal links
201
131
  return false if %r{^(/|\.\.)}.match?(href)
202
132
 
203
133
  absolute_different_domain?(link, site_domain)
204
134
  end
205
135
 
206
- # Add target="_blank" to external cross-domain links
207
- # Only adds if target is not already set
136
+ # Sets target="_blank" on a cross-domain link, unless it already has a target.
208
137
  def add_external_target_blank(link, site_domain)
209
- # Skip if link already has a target attribute
210
138
  return if link['target'].to_s.strip != ''
211
-
212
- # Add target="_blank" if this is a cross-domain external link
213
139
  return unless cross_domain_external_link?(link, site_domain)
214
140
 
215
141
  link['target'] = '_blank'
216
142
  end
217
143
 
218
- # Add rel="noopener noreferrer" to target="_blank" links, preserving
219
- # any existing rel attribute values
144
+ # Adds rel="noopener noreferrer" to a target="_blank" link, keeping any
145
+ # rel values the author already set.
220
146
  def add_external_target_protection(link)
221
147
  target = link['target'].to_s
222
-
223
- # Only add protection for target="_blank"
224
148
  return unless target == '_blank'
225
149
 
226
- # Get existing rel attribute (if any)
227
150
  existing_rel = link['rel'].to_s.strip
228
151
  rel_values = existing_rel.empty? ? [] : existing_rel.split(/\s+/)
229
-
230
- # Add required security values if not already present
231
152
  rel_values << 'noopener' unless rel_values.include?('noopener')
232
153
  rel_values << 'noreferrer' unless rel_values.include?('noreferrer')
233
154
 
234
- # Set the updated rel attribute
235
155
  link['rel'] = rel_values.join(' ')
236
156
  end
237
157
 
@@ -269,8 +189,8 @@ module Jekyll
269
189
  html
270
190
  end
271
191
 
272
- # Check if the plugin is enabled in configuration
273
- # Defaults to true if not explicitly configured
192
+ # Logs a notice when disabled; returns the given enabled flag unchanged.
193
+ # The caller (decorate_html) decides what "enabled" defaults to.
274
194
  def plugin_enabled?(enabled)
275
195
  unless enabled
276
196
  Jekyll.logger.info 'LinkDecorator:',
@@ -279,17 +199,16 @@ module Jekyll
279
199
  enabled
280
200
  end
281
201
 
282
- # Apply CSS classes to links based on their context (alert or default)
202
+ # Applies alert_classes to links inside an .alert box, default_classes
203
+ # to every other link.
283
204
  def apply_link_styles(doc, config)
284
205
  default_classes = config.fetch('default_link_classes', DEFAULT_LINK_CLASSES)
285
206
  alert_classes = config.fetch('alert_link_classes', DEFAULT_ALERT_LINK_CLASSES)
286
207
 
287
- # Apply alert classes to links inside p.alert or blockquote.alert
288
208
  doc.css('p.alert a:not(.btn), blockquote.alert a:not(.btn)').each do |link|
289
209
  add_classes(link, alert_classes)
290
210
  end
291
211
 
292
- # Apply default classes to all other non-alert links
293
212
  doc.css('a:not(.btn)').each do |link|
294
213
  next if link.ancestors('p.alert, blockquote.alert').any?
295
214
 
@@ -310,12 +229,12 @@ module Jekyll
310
229
  end
311
230
  end
312
231
 
313
- # Add CSS classes to a link element
314
232
  def add_classes(link, classes_string)
315
233
  classes_string.split.each { |cls| link.add_class(cls) }
316
234
  end
317
235
 
318
- # Add external link features: target="_blank", icons, and security attributes
236
+ # Adds target="_blank", the external-link icon, and rel security
237
+ # attributes to every link on the page, in that order.
319
238
  def add_external_link_features(doc, config)
320
239
  site_domain = extract_site_domain
321
240
  return if site_domain.empty?
@@ -327,14 +246,14 @@ module Jekyll
327
246
  end
328
247
  end
329
248
 
330
- # Extract site domain from Jekyll configuration
331
249
  def extract_site_domain
332
250
  site_url = @config['url'] || @config['baseurl'] || ''
333
251
  extract_domain(site_url)
334
252
  end
335
253
 
336
- # Inject a Font Awesome anchor link inside each heading (h1–h6).
337
- # Reads with_heading_anchor and with_heading_anchor_data from @config directly.
254
+ # Injects a permalink anchor into each heading (h1-h6). The anchor's
255
+ # glyph defaults to a plain "#"; with_heading_anchor_data can swap it
256
+ # for a Font Awesome icon or any other class list.
338
257
  def add_heading_anchors(doc)
339
258
  enabled = @config.key?('with_heading_anchor') ? @config['with_heading_anchor'] : DEFAULT_HEADING_ANCHOR
340
259
  return unless enabled
@@ -351,7 +270,7 @@ module Jekyll
351
270
  id = resolve_heading_id(heading, id_counts)
352
271
 
353
272
  anchor = Nokogiri::XML::Node.new('a', doc)
354
- anchor['class'] = 'heading-anchor mx-1'
273
+ anchor['class'] = 'heading-anchor p-1 mx-1 focus-ring'
355
274
  anchor['href'] = "##{id}"
356
275
  anchor['data-copy-anchor'] = "##{id}"
357
276
  anchor['aria-label'] = "Link to #{heading.text.strip}"
@@ -365,8 +284,9 @@ module Jekyll
365
284
  end
366
285
  end
367
286
 
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.
287
+ # Returns the heading's existing id, or generates one from its text and
288
+ # assigns it. id_counts tracks slugs already used, so repeated headings
289
+ # get unique "-1", "-2", ... suffixes.
370
290
  def resolve_heading_id(heading, id_counts)
371
291
  existing = heading['id'].to_s.strip
372
292
  return existing unless existing.empty?
@@ -379,8 +299,10 @@ module Jekyll
379
299
  final_id
380
300
  end
381
301
 
382
- # Convert heading text to a URL-safe slug matching the link.html.liquid algorithm:
383
- # strip → downcase → spaces to hyphens → remove ", ', ?, &, #, / → collapse hyphens
302
+ # Converts heading text to a URL-safe slug, loosely mirroring the slug
303
+ # rules in jekyll-theme-centos-base's link.html.liquid (same characters
304
+ # stripped and replaced), plus collapsing repeated/edge hyphens so
305
+ # headings with unusual punctuation still get a clean id.
384
306
  def slugify(text)
385
307
  text.strip
386
308
  .downcase
@@ -392,7 +314,9 @@ module Jekyll
392
314
 
393
315
  private :add_heading_anchors, :resolve_heading_id, :slugify
394
316
 
395
- # Add external-link icon to cross-domain links
317
+ # Appends the external-link icon to a cross-domain link, unless the
318
+ # icon is disabled, the link already has an icon-bearing tag excluded
319
+ # via external_link_icon_excluded_tags, or one is already present.
396
320
  def add_external_icon(link, site_domain, config, doc)
397
321
  external_link_icon = config.fetch('external_link_icon', DEFAULT_EXTERNAL_LINK_ICON)
398
322
 
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.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alain Reguera Delgado